260318:1135 Fix UUID #3
Build and Deploy / deploy (push) Successful in 8m43s

This commit is contained in:
admin
2026-03-18 11:35:51 +07:00
parent 6172b058df
commit 5d89079c2a
43 changed files with 1073 additions and 132 deletions
+85
View File
@@ -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
+19
View File
@@ -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
+18
View File
@@ -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
+18
View File
@@ -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
+19
View File
@@ -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
+22
View File
@@ -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
+21
View File
@@ -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
+21
View File
@@ -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`
+71
View File
@@ -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 |
+108
View File
@@ -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
+27
View File
@@ -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
+19
View File
@@ -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 { UpdateCirculationRoutingDto } from './dto/update-circulation-routing.dto';
import { SearchCirculationDto } from './dto/search-circulation.dto'; import { SearchCirculationDto } from './dto/search-circulation.dto';
import { DocumentNumberingService } from '../document-numbering/services/document-numbering.service'; import { DocumentNumberingService } from '../document-numbering/services/document-numbering.service';
import { Project } from '../project/entities/project.entity';
import { Correspondence } from '../correspondence/entities/correspondence.entity';
@Injectable() @Injectable()
export class CirculationService { export class CirculationService {
@@ -26,6 +28,58 @@ export class CirculationService {
private dataSource: DataSource 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) { async create(createDto: CreateCirculationDto, user: User) {
if (!user.primaryOrganizationId) { if (!user.primaryOrganizationId) {
throw new BadRequestException('User must belong to an organization'); throw new BadRequestException('User must belong to an organization');
@@ -36,9 +90,20 @@ export class CirculationService {
await queryRunner.startTransaction(); await queryRunner.startTransaction();
try { 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) // Generate No. using DocumentNumberingService (Type 900 - Circulation)
const result = await this.numberingService.generateNextNumber({ const result = await this.numberingService.generateNextNumber({
projectId: createDto.projectId || 0, // Use projectId from DTO or 0 projectId: resolvedProjectId,
originatorOrganizationId: user.primaryOrganizationId, originatorOrganizationId: user.primaryOrganizationId,
typeId: 900, // Fixed Type ID for Circulation typeId: 900, // Fixed Type ID for Circulation
year: new Date().getFullYear(), year: new Date().getFullYear(),
@@ -50,7 +115,7 @@ export class CirculationService {
const circulation = queryRunner.manager.create(Circulation, { const circulation = queryRunner.manager.create(Circulation, {
organizationId: user.primaryOrganizationId, organizationId: user.primaryOrganizationId,
correspondenceId: createDto.correspondenceId, correspondenceId: resolvedCorrId,
circulationNo: result.number, circulationNo: result.number,
subject: createDto.subject, subject: createDto.subject,
statusCode: 'OPEN', statusCode: 'OPEN',
@@ -58,13 +123,13 @@ export class CirculationService {
}); });
const savedCirculation = await queryRunner.manager.save(circulation); const savedCirculation = await queryRunner.manager.save(circulation);
if (createDto.assigneeIds && createDto.assigneeIds.length > 0) { if (resolvedAssigneeIds.length > 0) {
const routings = createDto.assigneeIds.map((userId, index) => const routings = resolvedAssigneeIds.map((assigneeId, index) =>
queryRunner.manager.create(CirculationRouting, { queryRunner.manager.create(CirculationRouting, {
circulationId: savedCirculation.id, circulationId: savedCirculation.id,
stepNumber: index + 1, stepNumber: index + 1,
organizationId: user.primaryOrganizationId, organizationId: user.primaryOrganizationId,
assignedTo: userId, assignedTo: assigneeId,
status: 'PENDING', status: 'PENDING',
}) })
); );
@@ -1,29 +1,25 @@
import { import {
IsInt,
IsString, IsString,
IsNotEmpty, IsNotEmpty,
IsArray, IsArray,
IsOptional, IsOptional,
ArrayMinSize, // ✅ เพิ่ม ArrayMinSize,
} from 'class-validator'; } from 'class-validator';
export class CreateCirculationDto { export class CreateCirculationDto {
@IsInt()
@IsNotEmpty() @IsNotEmpty()
correspondenceId!: number; // เอกสารต้นเรื่องที่จะเวียน correspondenceId!: number | string; // เอกสารต้นเรื่องที่จะเวียน (INT or UUID)
@IsInt()
@IsOptional() @IsOptional()
projectId?: number; // Project ID for Numbering projectId?: number | string; // Project ID or UUID for Numbering
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
subject!: string; // หัวข้อเรื่อง (Subject) subject!: string; // หัวข้อเรื่อง (Subject)
@IsArray() @IsArray()
@IsInt({ each: true })
@ArrayMinSize(1) // ✅ ต้องมีผู้รับอย่างน้อย 1 คน @ArrayMinSize(1) // ✅ ต้องมีผู้รับอย่างน้อย 1 คน
assigneeIds!: number[]; // รายชื่อ User ID ที่ต้องการส่งให้ (ผู้รับผิดชอบ) assigneeIds!: (number | string)[]; // รายชื่อ User ID or UUID ที่ต้องการส่งให้ (ADR-019)
@IsString() @IsString()
@IsOptional() @IsOptional()
@@ -35,6 +35,7 @@ import { WorkflowEngineService } from '../workflow-engine/workflow-engine.servic
import { UserService } from '../user/user.service'; import { UserService } from '../user/user.service';
import { SearchService } from '../search/search.service'; import { SearchService } from '../search/search.service';
import { FileStorageService } from '../../common/file-storage/file-storage.service'; import { FileStorageService } from '../../common/file-storage/file-storage.service';
import { Project } from '../project/entities/project.entity';
/** /**
* CorrespondenceService - Document management (CRUD) * CorrespondenceService - Document management (CRUD)
@@ -69,7 +70,52 @@ export class CorrespondenceService {
private fileStorageService: FileStorageService 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) { 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({ const type = await this.typeRepo.findOne({
where: { id: createDto.typeId }, where: { id: createDto.typeId },
}); });
@@ -94,7 +140,7 @@ export class CorrespondenceService {
} }
// Impersonation Logic // Impersonation Logic
if (createDto.originatorId && createDto.originatorId !== userOrgId) { if (resolvedOriginatorId && resolvedOriginatorId !== userOrgId) {
const permissions = await this.userService.getUserPermissions( const permissions = await this.userService.getUserPermissions(
user.user_id user.user_id
); );
@@ -103,7 +149,7 @@ export class CorrespondenceService {
'You do not have permission to create documents on behalf of other organizations.' 'You do not have permission to create documents on behalf of other organizations.'
); );
} }
userOrgId = createDto.originatorId; userOrgId = resolvedOriginatorId;
} }
if (!userOrgId) { if (!userOrgId) {
@@ -134,7 +180,7 @@ export class CorrespondenceService {
const orgCode = originatorOrg?.organizationCode ?? 'UNK'; const orgCode = originatorOrg?.organizationCode ?? 'UNK';
// [v1.5.1] Extract recipient organization from recipients array (Primary TO) // [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; const recipientOrganizationId = toRecipient?.organizationId;
let recipientCode = ''; let recipientCode = '';
@@ -146,7 +192,7 @@ export class CorrespondenceService {
} }
const docNumber = await this.numberingService.generateNextNumber({ const docNumber = await this.numberingService.generateNextNumber({
projectId: createDto.projectId, projectId: resolvedProjectId,
originatorOrganizationId: userOrgId, originatorOrganizationId: userOrgId,
typeId: createDto.typeId, typeId: createDto.typeId,
disciplineId: createDto.disciplineId, disciplineId: createDto.disciplineId,
@@ -165,7 +211,7 @@ export class CorrespondenceService {
correspondenceNumber: docNumber.number, correspondenceNumber: docNumber.number,
correspondenceTypeId: createDto.typeId, correspondenceTypeId: createDto.typeId,
disciplineId: createDto.disciplineId, disciplineId: createDto.disciplineId,
projectId: createDto.projectId, projectId: resolvedProjectId,
originatorId: userOrgId, originatorId: userOrgId,
isInternal: createDto.isInternal || false, isInternal: createDto.isInternal || false,
createdBy: user.user_id, createdBy: user.user_id,
@@ -195,9 +241,9 @@ export class CorrespondenceService {
}); });
await queryRunner.manager.save(revision); await queryRunner.manager.save(revision);
// Save Recipients // Save Recipients (using resolved INT IDs)
if (createDto.recipients && createDto.recipients.length > 0) { if (resolvedRecipients && resolvedRecipients.length > 0) {
const recipients = createDto.recipients.map((r) => const recipients = resolvedRecipients.map((r) =>
queryRunner.manager.create(CorrespondenceRecipient, { queryRunner.manager.create(CorrespondenceRecipient, {
correspondenceId: savedCorr.id, correspondenceId: savedCorr.id,
recipientOrganizationId: r.organizationId, 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 // 3. Update Correspondence Entity if needed
const correspondenceUpdate: DeepPartial<Correspondence> = {}; const correspondenceUpdate: DeepPartial<Correspondence> = {};
if (updateDto.disciplineId) if (updateDto.disciplineId)
correspondenceUpdate.disciplineId = updateDto.disciplineId; correspondenceUpdate.disciplineId = updateDto.disciplineId;
if (updateDto.projectId) if (updResolvedProjectId)
correspondenceUpdate.projectId = updateDto.projectId; correspondenceUpdate.projectId = updResolvedProjectId;
if (updateDto.originatorId) if (updResolvedOriginatorId)
correspondenceUpdate.originatorId = updateDto.originatorId; correspondenceUpdate.originatorId = updResolvedOriginatorId;
if (Object.keys(correspondenceUpdate).length > 0) { if (Object.keys(correspondenceUpdate).length > 0) {
await this.correspondenceRepo.update(id, correspondenceUpdate); await this.correspondenceRepo.update(id, correspondenceUpdate);
@@ -506,13 +568,13 @@ export class CorrespondenceService {
} }
// 5. Update Recipients if provided // 5. Update Recipients if provided
if (updateDto.recipients) { if (updResolvedRecipients) {
const recipientRepo = this.dataSource.getRepository( const recipientRepo = this.dataSource.getRepository(
CorrespondenceRecipient CorrespondenceRecipient
); );
await recipientRepo.delete({ correspondenceId: id }); await recipientRepo.delete({ correspondenceId: id });
const newRecipients = updateDto.recipients.map((r) => const newRecipients = updResolvedRecipients.map((r) =>
recipientRepo.create({ recipientRepo.create({
correspondenceId: id, correspondenceId: id,
recipientOrganizationId: r.organizationId, recipientOrganizationId: r.organizationId,
@@ -539,11 +601,11 @@ export class CorrespondenceService {
// Check for ACTUAL value changes // Check for ACTUAL value changes
const isProjectChanged = const isProjectChanged =
updateDto.projectId !== undefined && updResolvedProjectId !== undefined &&
updateDto.projectId !== currentCorr.projectId; updResolvedProjectId !== currentCorr.projectId;
const isOriginatorChanged = const isOriginatorChanged =
updateDto.originatorId !== undefined && updResolvedOriginatorId !== undefined &&
updateDto.originatorId !== currentCorr.originatorId; updResolvedOriginatorId !== currentCorr.originatorId;
const isDisciplineChanged = const isDisciplineChanged =
updateDto.disciplineId !== undefined && updateDto.disciplineId !== undefined &&
updateDto.disciplineId !== currentCorr.disciplineId; updateDto.disciplineId !== currentCorr.disciplineId;
@@ -554,15 +616,9 @@ export class CorrespondenceService {
let isRecipientChanged = false; let isRecipientChanged = false;
let newRecipientId: number | undefined; let newRecipientId: number | undefined;
if (updateDto.recipients) { if (updResolvedRecipients) {
// Safe check for 'type' or 'recipientType' (mismatch safeguard) const newToRecipient = updResolvedRecipients.find(
interface RecipientInput { (r) => r.type === 'TO'
type?: string;
recipientType?: string;
organizationId?: number;
}
const newToRecipient = updateDto.recipients.find(
(r: RecipientInput) => r.type === 'TO' || r.recipientType === 'TO'
); );
newRecipientId = newToRecipient?.organizationId; newRecipientId = newToRecipient?.organizationId;
@@ -594,7 +650,7 @@ export class CorrespondenceService {
// [Fix #6] Fetch real ORG Code from originator organization // [Fix #6] Fetch real ORG Code from originator organization
const originatorOrgForUpdate = await this.orgRepo.findOne({ const originatorOrgForUpdate = await this.orgRepo.findOne({
where: { where: {
id: updateDto.originatorId ?? currentCorr.originatorId ?? 0, id: updResolvedOriginatorId ?? currentCorr.originatorId ?? 0,
}, },
}); });
const orgCode = originatorOrgForUpdate?.organizationCode ?? 'UNK'; const orgCode = originatorOrgForUpdate?.organizationCode ?? 'UNK';
@@ -610,9 +666,9 @@ export class CorrespondenceService {
}; };
const newCtx = { const newCtx = {
projectId: updateDto.projectId ?? currentCorr.projectId, projectId: updResolvedProjectId ?? currentCorr.projectId,
originatorOrganizationId: originatorOrganizationId:
updateDto.originatorId ?? currentCorr.originatorId ?? 0, updResolvedOriginatorId ?? currentCorr.originatorId ?? 0,
typeId: updateDto.typeId ?? currentCorr.correspondenceTypeId, typeId: updateDto.typeId ?? currentCorr.correspondenceTypeId,
disciplineId: updateDto.disciplineId ?? currentCorr.disciplineId, disciplineId: updateDto.disciplineId ?? currentCorr.disciplineId,
recipientOrganizationId: targetRecipientId, recipientOrganizationId: targetRecipientId,
@@ -650,6 +706,20 @@ export class CorrespondenceService {
} }
async previewDocumentNumber(createDto: CreateCorrespondenceDto, user: User) { 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({ const type = await this.typeRepo.findOne({
where: { id: createDto.typeId }, where: { id: createDto.typeId },
}); });
@@ -661,13 +731,13 @@ export class CorrespondenceService {
if (fullUser) userOrgId = fullUser.primaryOrganizationId; if (fullUser) userOrgId = fullUser.primaryOrganizationId;
} }
if (createDto.originatorId && createDto.originatorId !== userOrgId) { if (previewOriginatorId && previewOriginatorId !== userOrgId) {
// Allow impersonation for preview // Allow impersonation for preview
userOrgId = createDto.originatorId; userOrgId = previewOriginatorId;
} }
// Extract recipient from recipients array // 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; const recipientOrganizationId = toRecipient?.organizationId;
let recipientCode = ''; let recipientCode = '';
@@ -679,7 +749,7 @@ export class CorrespondenceService {
} }
return this.numberingService.previewNumber({ return this.numberingService.previewNumber({
projectId: createDto.projectId, projectId: previewProjectId,
originatorOrganizationId: userOrgId!, originatorOrganizationId: userOrgId!,
typeId: createDto.typeId, typeId: createDto.typeId,
disciplineId: createDto.disciplineId, disciplineId: createDto.disciplineId,
@@ -11,10 +11,9 @@ import {
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class CreateCorrespondenceDto { export class CreateCorrespondenceDto {
@ApiProperty({ description: 'Project ID', example: 1 }) @ApiProperty({ description: 'Project ID or UUID', example: 1 })
@IsInt()
@IsNotEmpty() @IsNotEmpty()
projectId!: number; projectId!: number | string;
@ApiProperty({ description: 'Document Type ID', example: 1 }) @ApiProperty({ description: 'Document Type ID', example: 1 })
@IsInt() @IsInt()
@@ -110,12 +109,11 @@ export class CreateCorrespondenceDto {
// ✅ เพิ่ม Field สำหรับ Impersonation (เลือกองค์กรผู้ส่ง) // ✅ เพิ่ม Field สำหรับ Impersonation (เลือกองค์กรผู้ส่ง)
@ApiPropertyOptional({ @ApiPropertyOptional({
description: 'Originator Organization ID (for impersonation)', description: 'Originator Organization ID or UUID (for impersonation)',
example: 1, example: 1,
}) })
@IsInt()
@IsOptional() @IsOptional()
originatorId?: number; originatorId?: number | string;
@ApiPropertyOptional({ @ApiPropertyOptional({
description: 'Recipients', description: 'Recipients',
@@ -123,5 +121,5 @@ export class CreateCorrespondenceDto {
}) })
@IsArray() @IsArray()
@IsOptional() @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 {
import { InjectRepository } from '@nestjs/typeorm'; Injectable,
import { Repository } from 'typeorm'; Logger,
BadRequestException,
NotFoundException,
} from '@nestjs/common';
import { InjectRepository, InjectEntityManager } from '@nestjs/typeorm';
import { Repository, EntityManager } from 'typeorm';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { DocumentNumberFormat } from '../entities/document-number-format.entity'; 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 { GenerateNumberContext } from '../interfaces/document-numbering.interface';
import { ReserveNumberDto } from '../dto/reserve-number.dto'; import { ReserveNumberDto } from '../dto/reserve-number.dto';
import { ConfirmReservationDto } from '../dto/confirm-reservation.dto'; import { ConfirmReservationDto } from '../dto/confirm-reservation.dto';
import { Project } from '../../project/entities/project.entity';
@Injectable() @Injectable()
export class DocumentNumberingService { export class DocumentNumberingService {
@@ -39,9 +45,27 @@ export class DocumentNumberingService {
private lockService: DocumentNumberingLockService, private lockService: DocumentNumberingLockService,
private configService: ConfigService, private configService: ConfigService,
private manualOverrideService: ManualOverrideService, 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( async generateNextNumber(
ctx: GenerateNumberContext ctx: GenerateNumberContext
): Promise<{ number: string; auditId: number }> { ): Promise<{ number: string; auditId: number }> {
@@ -218,11 +242,15 @@ export class DocumentNumberingService {
return this.formatRepo.find(); return this.formatRepo.find();
} }
async getTemplatesByProject(projectId: number) { async getTemplatesByProject(projectId: number | string) {
return this.formatRepo.find({ where: { projectId } }); const internalId = await this.resolveProjectId(projectId);
return this.formatRepo.find({ where: { projectId: internalId } });
} }
async saveTemplate(dto: any) { async saveTemplate(dto: any) {
if (dto.projectId) {
dto.projectId = await this.resolveProjectId(dto.projectId);
}
return this.formatRepo.save(dto); return this.formatRepo.save(dto);
} }
@@ -6,7 +6,10 @@ import {
UpdateDateColumn, UpdateDateColumn,
DeleteDateColumn, DeleteDateColumn,
Unique, Unique,
ManyToOne,
JoinColumn,
} from 'typeorm'; } from 'typeorm';
import { Project } from '../../project/entities/project.entity';
@Entity('tags') @Entity('tags')
@Unique('ux_tag_project', ['project_id', 'tag_name']) @Unique('ux_tag_project', ['project_id', 'tag_name'])
@@ -26,6 +29,11 @@ export class Tag {
@Column({ type: 'text', nullable: true }) @Column({ type: 'text', nullable: true })
description!: string | null; // เพิ่ม ! description!: string | null; // เพิ่ม !
// Relations
@ManyToOne(() => Project)
@JoinColumn({ name: 'project_id' })
project?: Project;
@CreateDateColumn() @CreateDateColumn()
created_at!: Date; // เพิ่ม ! created_at!: Date; // เพิ่ม !
+4 -1
View File
@@ -132,6 +132,7 @@ export class MasterService {
} }
return this.rfaTypeRepo.find({ return this.rfaTypeRepo.find({
where, where,
relations: ['contract'],
order: { typeCode: 'ASC' }, order: { typeCode: 'ASC' },
}); });
} }
@@ -296,7 +297,9 @@ export class MasterService {
} }
async findAllTags(query?: SearchTagDto) { async findAllTags(query?: SearchTagDto) {
const qb = this.tagRepo.createQueryBuilder('tag'); const qb = this.tagRepo
.createQueryBuilder('tag')
.leftJoinAndSelect('tag.project', 'project');
if (query?.project_id) { if (query?.project_id) {
// In Tags, we use project_id (INT) directly or resolve if UUID passed via query // 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') @Entity('rfa_types')
export class RfaType { export class RfaType {
@@ -23,6 +31,11 @@ export class RfaType {
@Column({ name: 'is_active', default: true }) @Column({ name: 'is_active', default: true })
isActive!: boolean; isActive!: boolean;
// Relations
@ManyToOne(() => Contract)
@JoinColumn({ name: 'contract_id' })
contract?: Contract;
// Virtual property for backward compatibility // Virtual property for backward compatibility
typeName!: string; typeName!: string;
@@ -45,7 +45,7 @@ export default function DisciplinesPage() {
const contractOptions = contracts.map((c: any) => ({ const contractOptions = contracts.map((c: any) => ({
label: `${c.contractName} (${c.contractCode})`, label: `${c.contractName} (${c.contractCode})`,
value: String(c.id || c.uuid), value: String(c.id),
})); }));
return ( return (
@@ -55,9 +55,16 @@ export default function DisciplinesPage() {
title="Disciplines Management" title="Disciplines Management"
description="Manage system disciplines (e.g., ARCH, STR, MEC)" description="Manage system disciplines (e.g., ARCH, STR, MEC)"
queryKey={['disciplines', selectedContractId ?? 'all']} 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)} 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)} deleteFn={(id) => masterDataService.deleteDiscipline(id)}
columns={columns} columns={columns}
filters={ filters={
@@ -72,7 +79,7 @@ export default function DisciplinesPage() {
<SelectContent> <SelectContent>
<SelectItem value="all">All Contracts</SelectItem> <SelectItem value="all">All Contracts</SelectItem>
{contracts.map((c: any) => ( {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}) {c.contractName} ({c.contractCode})
</SelectItem> </SelectItem>
))} ))}
@@ -49,7 +49,7 @@ export default function RfaTypesPage() {
const contractOptions = contracts.map((c: any) => ({ const contractOptions = contracts.map((c: any) => ({
label: `${c.contractName} (${c.contractCode})`, label: `${c.contractName} (${c.contractCode})`,
value: String(c.id || c.uuid), value: String(c.id),
})); }));
return ( return (
@@ -58,7 +58,14 @@ export default function RfaTypesPage() {
entityName="RFA Type" entityName="RFA Type"
title="RFA Types Management" title="RFA Types Management"
queryKey={['rfa-types', selectedContractId ?? 'all']} 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)} createFn={(data: Record<string, unknown>) => masterDataService.createRfaType(data as any)}
updateFn={(id, data) => masterDataService.updateRfaType(id, data)} updateFn={(id, data) => masterDataService.updateRfaType(id, data)}
deleteFn={(id) => masterDataService.deleteRfaType(id)} deleteFn={(id) => masterDataService.deleteRfaType(id)}
@@ -75,7 +82,7 @@ export default function RfaTypesPage() {
<SelectContent> <SelectContent>
<SelectItem value="all">All Contracts</SelectItem> <SelectItem value="all">All Contracts</SelectItem>
{contracts.map((c: any) => ( {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}) {c.contractName} ({c.contractCode})
</SelectItem> </SelectItem>
))} ))}
@@ -16,8 +16,8 @@ export default function TagsPage() {
const projectOptions = [ const projectOptions = [
{ label: "Global (All Projects)", value: "__none__" }, { label: "Global (All Projects)", value: "__none__" },
...(projectsData || []).map((p: Record<string, unknown>) => ({ ...(projectsData || []).map((p: Record<string, unknown>) => ({
label: p.projectName || p.projectCode || p.project_name || p.project_code || `Project ${p.id}`, label: (p.projectName || p.projectCode || p.project_name || p.project_code || `Project ${p.id}`) as string,
value: String(p.id), value: String(p.id), // p.id = UUID string via serialization
})), })),
]; ];
@@ -26,10 +26,10 @@ export default function TagsPage() {
accessorKey: "project_id", accessorKey: "project_id",
header: "Project", header: "Project",
cell: ({ row }) => { cell: ({ row }) => {
const pId = row.original.project_id; const item = row.original as Record<string, unknown>;
if (!pId) return <span className="text-muted-foreground italic">Global</span>; const project = item.project as Record<string, unknown> | null;
const p = (projectsData || []).find((proj: Record<string, unknown>) => proj.id === pId); if (!project) return <span className="text-muted-foreground italic">Global</span>;
return p ? (p.projectName || p.projectCode || p.project_name || p.project_code || `Project ${pId}`) as React.ReactNode : pId as React.ReactNode; 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." description="Manage system tags, multi-tenant capable."
entityName="Tag" entityName="Tag"
queryKey={["tags"]} 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)} createFn={(data: Record<string, unknown>) => masterDataService.createTag(formatPayload(data) as unknown as CreateTagDto)}
updateFn={(id, data) => masterDataService.updateTag(id, formatPayload(data))} updateFn={(id, data) => masterDataService.updateTag(id, formatPayload(data))}
deleteFn={(id) => masterDataService.deleteTag(id)} deleteFn={(id) => masterDataService.deleteTag(id)}
@@ -45,9 +45,9 @@ import { cn } from "@/lib/utils";
// Form validation schema // Form validation schema
const formSchema = z.object({ const formSchema = z.object({
correspondenceId: z.number(), correspondenceId: z.string().min(1, "Please select a document"),
subject: z.string().min(1, "Subject is required"), 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(), remarks: z.string().optional(),
}); });
@@ -98,18 +98,18 @@ export default function CreateCirculationPage() {
const selectedDocId = form.watch("correspondenceId"); const selectedDocId = form.watch("correspondenceId");
const selectedDoc = correspondences?.data?.find( 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"); const current = form.getValues("assigneeIds");
if (current.includes(userId)) { if (current.includes(userUuid)) {
form.setValue( form.setValue(
"assigneeIds", "assigneeIds",
current.filter((id) => id !== userId) current.filter((id) => id !== userUuid)
); );
} else { } else {
form.setValue("assigneeIds", [...current, userId]); form.setValue("assigneeIds", [...current, userUuid]);
} }
}; };
@@ -168,19 +168,19 @@ export default function CreateCirculationPage() {
<CommandList> <CommandList>
<CommandEmpty>No document found.</CommandEmpty> <CommandEmpty>No document found.</CommandEmpty>
<CommandGroup> <CommandGroup>
{correspondences?.data?.map((doc: { id: number; correspondenceNumber: string }) => ( {correspondences?.data?.map((doc: { uuid: string; correspondenceNumber: string }) => (
<CommandItem <CommandItem
key={doc.id} key={doc.uuid}
value={doc.correspondenceNumber} value={doc.correspondenceNumber}
onSelect={() => { onSelect={() => {
form.setValue("correspondenceId", doc.id); form.setValue("correspondenceId", doc.uuid);
setDocOpen(false); setDocOpen(false);
}} }}
> >
<Check <Check
className={cn( className={cn(
"mr-2 h-4 w-4", "mr-2 h-4 w-4",
doc.id === field.value doc.uuid === field.value
? "opacity-100" ? "opacity-100"
: "opacity-0" : "opacity-0"
)} )}
@@ -230,13 +230,13 @@ export default function CreateCirculationPage() {
> >
{selectedAssignees.length > 0 ? ( {selectedAssignees.length > 0 ? (
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">
{selectedAssignees.map((userId) => { {selectedAssignees.map((userUuid) => {
const user = users.find( const user = users.find(
(u) => u.userId === userId (u) => u.uuid === userUuid
); );
return user ? ( return user ? (
<Badge <Badge
key={userId} key={userUuid}
variant="secondary" variant="secondary"
className="mr-1" className="mr-1"
> >
@@ -245,7 +245,7 @@ export default function CreateCirculationPage() {
className="ml-1 h-3 w-3 cursor-pointer" className="ml-1 h-3 w-3 cursor-pointer"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
toggleAssignee(userId); toggleAssignee(userUuid);
}} }}
/> />
</Badge> </Badge>
@@ -269,14 +269,14 @@ export default function CreateCirculationPage() {
<CommandGroup> <CommandGroup>
{users.map((user) => ( {users.map((user) => (
<CommandItem <CommandItem
key={user.userId ?? user.uuid} key={user.uuid}
value={user.username} value={user.username}
onSelect={() => user.userId && toggleAssignee(user.userId)} onSelect={() => toggleAssignee(user.uuid)}
> >
<Check <Check
className={cn( className={cn(
"mr-2 h-4 w-4", "mr-2 h-4 w-4",
user.userId != null && selectedAssignees.includes(user.userId) selectedAssignees.includes(user.uuid)
? "opacity-100" ? "opacity-100"
: "opacity-0" : "opacity-0"
)} )}
+16 -14
View File
@@ -26,7 +26,7 @@ import { correspondenceService } from "@/lib/services/correspondence.service";
// Updated Zod Schema with all required fields // Updated Zod Schema with all required fields
const correspondenceSchema = z.object({ 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"), documentTypeId: z.number().min(1, "Please select a Document Type"),
disciplineId: z.number().optional(), disciplineId: z.number().optional(),
subject: z.string().min(5, "Subject must be at least 5 characters"), subject: z.string().min(5, "Subject must be at least 5 characters"),
@@ -34,8 +34,8 @@ const correspondenceSchema = z.object({
body: z.string().optional(), body: z.string().optional(),
remarks: z.string().optional(), remarks: z.string().optional(),
dueDate: z.string().optional(), // ISO Date string dueDate: z.string().optional(), // ISO Date string
fromOrganizationId: z.number().min(1, "Please select From Organization"), fromOrganizationId: z.string().min(1, "Please select From Organization"),
toOrganizationId: z.number().min(1, "Please select To Organization"), toOrganizationId: z.string().min(1, "Please select To Organization"),
importance: z.enum(["NORMAL", "HIGH", "URGENT"]), importance: z.enum(["NORMAL", "HIGH", "URGENT"]),
attachments: z.array(z.instanceof(File)).optional(), attachments: z.array(z.instanceof(File)).optional(),
}); });
@@ -56,7 +56,7 @@ export function CorrespondenceForm({ initialData, uuid }: { initialData?: any, u
// Extract initial values if editing // Extract initial values if editing
const currentRev = initialData?.revisions?.find((r: any) => r.isCurrent) || initialData?.revisions?.[0]; const currentRev = initialData?.revisions?.find((r: any) => r.isCurrent) || initialData?.revisions?.[0];
const defaultValues: Partial<FormData> = { const defaultValues: Partial<FormData> = {
projectId: initialData?.projectId || undefined, projectId: initialData?.projectId ? String(initialData.projectId) : undefined,
documentTypeId: initialData?.correspondenceTypeId || undefined, documentTypeId: initialData?.correspondenceTypeId || undefined,
disciplineId: initialData?.disciplineId || undefined, disciplineId: initialData?.disciplineId || undefined,
subject: currentRev?.subject || currentRev?.title || "", subject: currentRev?.subject || currentRev?.title || "",
@@ -64,9 +64,11 @@ export function CorrespondenceForm({ initialData, uuid }: { initialData?: any, u
body: currentRev?.body || "", body: currentRev?.body || "",
remarks: currentRev?.remarks || "", remarks: currentRev?.remarks || "",
dueDate: currentRev?.dueDate ? new Date(currentRev.dueDate).toISOString().split('T')[0] : undefined, 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 // 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", importance: currentRev?.details?.importance || "NORMAL",
}; };
@@ -209,8 +211,8 @@ export function CorrespondenceForm({ initialData, uuid }: { initialData?: any, u
<div className="space-y-2"> <div className="space-y-2">
<Label>Project *</Label> <Label>Project *</Label>
<Select <Select
onValueChange={(v) => setValue("projectId", parseInt(v))} onValueChange={(v) => setValue("projectId", v)}
value={projectId ? String(projectId) : undefined} value={projectId || undefined}
disabled={isLoadingProjects} disabled={isLoadingProjects}
> >
<SelectTrigger> <SelectTrigger>
@@ -323,8 +325,8 @@ export function CorrespondenceForm({ initialData, uuid }: { initialData?: any, u
<div className="space-y-2"> <div className="space-y-2">
<Label>From Organization *</Label> <Label>From Organization *</Label>
<Select <Select
onValueChange={(v) => setValue("fromOrganizationId", parseInt(v))} onValueChange={(v) => setValue("fromOrganizationId", v)}
value={fromOrgId ? String(fromOrgId) : undefined} value={fromOrgId || undefined}
disabled={isLoadingOrgs} disabled={isLoadingOrgs}
> >
<SelectTrigger> <SelectTrigger>
@@ -332,7 +334,7 @@ export function CorrespondenceForm({ initialData, uuid }: { initialData?: any, u
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{(organizations || []).map((org: Organization) => ( {(organizations || []).map((org: Organization) => (
<SelectItem key={org.id} value={String(org.id)}> <SelectItem key={org.uuid} value={org.uuid}>
{org.organizationName} ({org.organizationCode}) {org.organizationName} ({org.organizationCode})
</SelectItem> </SelectItem>
))} ))}
@@ -346,8 +348,8 @@ export function CorrespondenceForm({ initialData, uuid }: { initialData?: any, u
<div className="space-y-2"> <div className="space-y-2">
<Label>To Organization *</Label> <Label>To Organization *</Label>
<Select <Select
onValueChange={(v) => setValue("toOrganizationId", parseInt(v))} onValueChange={(v) => setValue("toOrganizationId", v)}
value={toOrgId ? String(toOrgId) : undefined} value={toOrgId || undefined}
disabled={isLoadingOrgs} disabled={isLoadingOrgs}
> >
<SelectTrigger> <SelectTrigger>
@@ -355,7 +357,7 @@ export function CorrespondenceForm({ initialData, uuid }: { initialData?: any, u
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{(organizations || []).map((org: Organization) => ( {(organizations || []).map((org: Organization) => (
<SelectItem key={org.id} value={String(org.id)}> <SelectItem key={org.uuid} value={org.uuid}>
{org.organizationName} ({org.organizationCode}) {org.organizationName} ({org.organizationCode})
</SelectItem> </SelectItem>
))} ))}
+4 -1
View File
@@ -4,6 +4,7 @@ import { ColumnDef } from '@tanstack/react-table';
import { Drawing } from '@/types/drawing'; import { Drawing } from '@/types/drawing';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { ArrowUpDown, MoreHorizontal } from 'lucide-react'; import { ArrowUpDown, MoreHorizontal } from 'lucide-react';
import Link from 'next/link';
import { import {
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
@@ -66,7 +67,9 @@ export const columns: ColumnDef<Drawing>[] = [
Copy Drawing No. Copy Drawing No.
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem>View Details</DropdownMenuItem> <DropdownMenuItem asChild>
<Link href={`/drawings/${drawing.uuid}`}>View Details</Link>
</DropdownMenuItem>
{/* Add download/view functionality later */} {/* Add download/view functionality later */}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
@@ -68,8 +68,8 @@ export function TemplateTester({ open, onOpenChange, template }: TemplateTesterP
try { try {
const result = await numberingApi.previewNumber({ const result = await numberingApi.previewNumber({
projectId: projectId, projectId: projectId,
originatorOrganizationId: parseInt(testData.originatorId || "0"), originatorOrganizationId: testData.originatorId || "0",
recipientOrganizationId: parseInt(testData.recipientId || "0"), recipientOrganizationId: testData.recipientId || "0",
correspondenceTypeId: parseInt(testData.correspondenceTypeId || "0"), correspondenceTypeId: parseInt(testData.correspondenceTypeId || "0"),
disciplineId: parseInt(testData.disciplineId || "0"), disciplineId: parseInt(testData.disciplineId || "0"),
}); });
+3 -3
View File
@@ -33,7 +33,7 @@ export function useDrawings(type: DrawingType, params: DrawingSearchParams) {
if (response && response.data) { if (response && response.data) {
const mappedData = response.data.map((d: ContractDrawing) => ({ const mappedData = response.data.map((d: ContractDrawing) => ({
...d, ...d,
uuid: d.uuid, uuid: d.uuid || (d as unknown as { id: string }).id,
drawingNumber: d.contractDrawingNo, drawingNumber: d.contractDrawingNo,
type: 'CONTRACT', type: 'CONTRACT',
})); }));
@@ -46,7 +46,7 @@ export function useDrawings(type: DrawingType, params: DrawingSearchParams) {
if (response && response.data) { if (response && response.data) {
const mappedData = response.data.map((d: ShopDrawing) => ({ const mappedData = response.data.map((d: ShopDrawing) => ({
...d, ...d,
uuid: d.uuid, uuid: d.uuid || (d as unknown as { id: string }).id,
type: 'SHOP', type: 'SHOP',
title: d.currentRevision?.title || 'Untitled', title: d.currentRevision?.title || 'Untitled',
revision: d.currentRevision?.revisionNumber, revision: d.currentRevision?.revisionNumber,
@@ -61,7 +61,7 @@ export function useDrawings(type: DrawingType, params: DrawingSearchParams) {
if (response && response.data) { if (response && response.data) {
const mappedData = response.data.map((d: AsBuiltDrawing) => ({ const mappedData = response.data.map((d: AsBuiltDrawing) => ({
...d, ...d,
uuid: d.uuid, uuid: d.uuid || (d as unknown as { id: string }).id,
type: 'AS_BUILT', type: 'AS_BUILT',
title: d.currentRevision?.title || 'Untitled', title: d.currentRevision?.title || 'Untitled',
revision: d.currentRevision?.revisionNumber, revision: d.currentRevision?.revisionNumber,
+4 -4
View File
@@ -34,7 +34,7 @@ export interface NumberingTemplate {
*/ */
export interface SaveTemplateDto { export interface SaveTemplateDto {
id?: number; // If present, update; otherwise create id?: number; // If present, update; otherwise create
projectId: number; projectId: number | string;
correspondenceTypeId: number | null; correspondenceTypeId: number | null;
formatTemplate: string; formatTemplate: string;
description?: string; description?: string;
@@ -273,13 +273,13 @@ export const numberingApi = {
* Preview what a document number would look like (without generating) * Preview what a document number would look like (without generating)
*/ */
previewNumber: async (ctx: { previewNumber: async (ctx: {
projectId: number; projectId: number | string;
originatorOrganizationId: number; originatorOrganizationId: number | string;
correspondenceTypeId: number; correspondenceTypeId: number;
disciplineId?: number; disciplineId?: number;
subTypeId?: number; subTypeId?: number;
rfaTypeId?: number; rfaTypeId?: number;
recipientOrganizationId?: number; recipientOrganizationId?: number | string;
}): Promise<{ previewNumber: string; nextSequence: number }> => { }): Promise<{ previewNumber: string; nextSequence: number }> => {
const res = await apiClient.post<{ data: { previewNumber: string; nextSequence: number } }>( const res = await apiClient.post<{ data: { previewNumber: string; nextSequence: number } }>(
'/document-numbering/preview', '/document-numbering/preview',
+3 -3
View File
@@ -89,10 +89,10 @@ export interface CirculationListResponse {
* DTO for creating a circulation * DTO for creating a circulation
*/ */
export interface CreateCirculationDto { export interface CreateCirculationDto {
correspondenceId: number; correspondenceId: number | string;
projectId?: number; projectId?: number | string;
subject: string; subject: string;
assigneeIds: number[]; assigneeIds: (number | string)[];
remarks?: string; remarks?: string;
} }
@@ -1,15 +1,15 @@
// File: src/types/dto/circulation/create-circulation.dto.ts // File: src/types/dto/circulation/create-circulation.dto.ts
export interface CreateCirculationDto { export interface CreateCirculationDto {
/** เอกสารต้นเรื่องที่จะเวียน (Correspondence ID) */ /** เอกสารต้นเรื่องที่จะเวียน (Correspondence ID or UUID) */
correspondenceId: number; correspondenceId: number | string;
/** หัวข้อเรื่อง (Subject) */ /** หัวข้อเรื่อง (Subject) */
subject: string; subject: string;
/** รายชื่อ User ID ที่ต้องการส่งให้ (ผู้รับผิดชอบ) */ /** รายชื่อ User ID/UUID ที่ต้องการส่งให้ (ผู้รับผิดชอบ) */
assigneeIds: number[]; assigneeIds: (number | string)[];
/** หมายเหตุเพิ่มเติม (ถ้ามี) */ /** หมายเหตุเพิ่มเติม (ถ้ามี) */
remarks?: string; remarks?: string;
} }
@@ -1,8 +1,8 @@
// File: src/types/dto/correspondence/create-correspondence.dto.ts // File: src/types/dto/correspondence/create-correspondence.dto.ts
export interface CreateCorrespondenceDto { export interface CreateCorrespondenceDto {
/** ID ของโครงการ */ /** ID or UUID ของโครงการ */
projectId: number; projectId: number | string;
/** ID ของประเภทเอกสาร (เช่น RFA, LETTER) */ /** ID ของประเภทเอกสาร (เช่น RFA, LETTER) */
typeId: number; typeId: number;
@@ -37,8 +37,8 @@ export interface CreateCorrespondenceDto {
/** * Field Impersonation () /** * Field Impersonation ()
* Admin * Admin
*/ */
originatorId?: number; originatorId?: number | string;
/** รายชื่อผู้รับ */ /** รายชื่อผู้รับ */
recipients?: { organizationId: number; type: 'TO' | 'CC' }[]; recipients?: { organizationId: number | string; type: 'TO' | 'CC' }[];
} }