690329:1250 Fixing bugs uuid by Kimi K2.5 #04
This commit is contained in:
@@ -5,7 +5,7 @@ export class CreateTagDto {
|
||||
@ApiProperty({ example: 'URGENT', description: 'ชื่อ Tag' })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
tag_name!: string; // เพิ่ม !
|
||||
tagName!: string;
|
||||
|
||||
@ApiProperty({ example: 'คำอธิบาย', description: 'คำอธิบาย' })
|
||||
@IsString()
|
||||
@@ -19,7 +19,7 @@ export class CreateTagDto {
|
||||
})
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
color_code?: string;
|
||||
colorCode?: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: 1,
|
||||
@@ -27,5 +27,5 @@ export class CreateTagDto {
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
project_id?: number | string;
|
||||
projectId?: number | string;
|
||||
}
|
||||
|
||||
@@ -2,16 +2,17 @@
|
||||
|
||||
import { IsString, IsOptional, IsInt, Min } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { ApiPropertyOptional, ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class SearchTagDto {
|
||||
@ApiPropertyOptional({
|
||||
description: 'ID โครงการ (ใช้กรอง Tag ของแต่ละโปรเจกต์)',
|
||||
@ApiProperty({
|
||||
example: 1,
|
||||
description:
|
||||
'Project ID or UUID (ใช้กรอง Tag ของแต่ละโปรเจกต์) - ADR-019: Accept UUID',
|
||||
required: false,
|
||||
})
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
project_id?: number;
|
||||
projectId?: number | string;
|
||||
|
||||
@ApiPropertyOptional({ description: 'คำค้นหา (ชื่อ Tag หรือ คำอธิบาย)' })
|
||||
@IsOptional()
|
||||
|
||||
@@ -303,25 +303,24 @@ export class MasterService {
|
||||
.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
|
||||
if (query?.projectId) {
|
||||
const internalId = await this.uuidResolver.resolveProjectId(
|
||||
query.project_id
|
||||
query.projectId
|
||||
);
|
||||
qb.andWhere('tag.project_id = :projectId', {
|
||||
qb.andWhere('tag.projectId = :projectId', {
|
||||
projectId: internalId,
|
||||
});
|
||||
}
|
||||
|
||||
if (query?.search) {
|
||||
qb.andWhere(
|
||||
'(tag.tag_name LIKE :search OR tag.description LIKE :search)',
|
||||
'(tag.tagName LIKE :search OR tag.description LIKE :search)',
|
||||
{
|
||||
search: `%${query.search}%`,
|
||||
}
|
||||
);
|
||||
}
|
||||
qb.orderBy('tag.tag_name', 'ASC');
|
||||
qb.orderBy('tag.tagName', 'ASC');
|
||||
if (query?.page && query?.limit) {
|
||||
const page = query.page;
|
||||
const limit = query.limit;
|
||||
@@ -342,11 +341,13 @@ export class MasterService {
|
||||
}
|
||||
|
||||
async createTag(dto: CreateTagDto, userId: number) {
|
||||
const internalProjectId = dto.project_id
|
||||
? await this.uuidResolver.resolveProjectId(dto.project_id)
|
||||
const internalProjectId = dto.projectId
|
||||
? await this.uuidResolver.resolveProjectId(dto.projectId)
|
||||
: null;
|
||||
const tag = this.tagRepo.create({
|
||||
...dto,
|
||||
tagName: dto.tagName,
|
||||
colorCode: dto.colorCode,
|
||||
description: dto.description,
|
||||
projectId: internalProjectId,
|
||||
createdBy: userId,
|
||||
});
|
||||
@@ -355,10 +356,18 @@ export class MasterService {
|
||||
|
||||
async updateTag(id: number, dto: UpdateTagDto) {
|
||||
const tag = await this.findOneTag(id);
|
||||
if (dto.project_id) {
|
||||
dto.project_id = await this.uuidResolver.resolveProjectId(dto.project_id);
|
||||
let internalProjectId = dto.projectId;
|
||||
if (dto.projectId) {
|
||||
internalProjectId = await this.uuidResolver.resolveProjectId(
|
||||
dto.projectId
|
||||
);
|
||||
}
|
||||
Object.assign(tag, dto);
|
||||
Object.assign(tag, {
|
||||
tagName: dto.tagName,
|
||||
colorCode: dto.colorCode,
|
||||
description: dto.description,
|
||||
projectId: internalProjectId,
|
||||
});
|
||||
return this.tagRepo.save(tag);
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ export default function TagsPage() {
|
||||
|
||||
const columns: ColumnDef<Tag>[] = [
|
||||
{
|
||||
accessorKey: 'project_id',
|
||||
accessorKey: 'projectId',
|
||||
header: 'Project',
|
||||
cell: ({ row }) => {
|
||||
const item = row.original as Tag & { project?: { id?: number | string; publicId?: string; projectName?: string; projectCode?: string } };
|
||||
@@ -34,7 +34,7 @@ export default function TagsPage() {
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'tag_name',
|
||||
accessorKey: 'tagName',
|
||||
header: 'Tag Name',
|
||||
cell: ({ row }) => {
|
||||
const color = String(row.original.colorCode || 'default');
|
||||
@@ -58,9 +58,9 @@ export default function TagsPage() {
|
||||
|
||||
const formatPayload = (data: Record<string, unknown>) => {
|
||||
const payload = { ...data };
|
||||
// ADR-019: project_id is now a UUID string or '__none__' for global
|
||||
if (!payload.project_id || payload.project_id === '__none__') {
|
||||
payload.project_id = null;
|
||||
// ADR-019: projectId is now a UUID string or '__none__' for global
|
||||
if (!payload.projectId || payload.projectId === '__none__') {
|
||||
payload.projectId = null;
|
||||
}
|
||||
return payload;
|
||||
};
|
||||
@@ -73,12 +73,12 @@ export default function TagsPage() {
|
||||
queryKey={['tags']}
|
||||
fetchFn={async () => {
|
||||
const items = await masterDataService.getTags();
|
||||
// ADR-019: Map project_id INT → project UUID for edit mode select matching
|
||||
// ADR-019: Map projectId INT → project UUID for edit mode select matching
|
||||
return items.map((item) => {
|
||||
const rec = item as Tag & { project?: { id?: number | string; publicId?: string }; project_id?: number | string };
|
||||
const rec = item as Tag & { project?: { id?: number | string; publicId?: string }; projectId?: number | string };
|
||||
return {
|
||||
...item,
|
||||
project_id: rec.project?.publicId || rec.project?.id || (rec.project_id ? String(rec.project_id) : null),
|
||||
projectId: rec.project?.publicId || rec.project?.id || (rec.projectId ? String(rec.projectId) : null),
|
||||
} as Tag;
|
||||
});
|
||||
}}
|
||||
@@ -90,7 +90,7 @@ export default function TagsPage() {
|
||||
columns={columns}
|
||||
fields={[
|
||||
{
|
||||
name: 'project_id',
|
||||
name: 'projectId',
|
||||
label: 'Project Scope',
|
||||
type: 'select',
|
||||
options: projectOptions,
|
||||
|
||||
@@ -439,81 +439,6 @@ export function CorrespondenceForm({ initialData, uuid }: { initialData?: Initia
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Subject */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="subject">Subject *</Label>
|
||||
<Input id="subject" {...register('subject')} placeholder="Enter subject" />
|
||||
{errors.subject && <p className="text-sm text-destructive">{errors.subject.message}</p>}
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="body">Body (Content)</Label>
|
||||
<Textarea id="body" {...register('body')} rows={6} placeholder="Enter letter content..." />
|
||||
</div>
|
||||
|
||||
{/* Date Fields */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="documentDate">Document Date</Label>
|
||||
<Input
|
||||
id="documentDate"
|
||||
type="date"
|
||||
{...register('documentDate')}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
setValue('documentDate', val, { shouldValidate: true, shouldDirty: true });
|
||||
if (val) {
|
||||
setValue('issuedDate', val, { shouldValidate: true, shouldDirty: true });
|
||||
setValue('receivedDate', val, { shouldValidate: true, shouldDirty: true });
|
||||
const d = new Date(val);
|
||||
d.setDate(d.getDate() + 7);
|
||||
setValue('dueDate', d.toISOString().split('T')[0], { shouldValidate: true, shouldDirty: true });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="issuedDate">Issued Date</Label>
|
||||
<Input id="issuedDate" type="date" {...register('issuedDate')} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="receivedDate">Received Date</Label>
|
||||
<Input
|
||||
id="receivedDate"
|
||||
type="date"
|
||||
{...register('receivedDate')}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
setValue('receivedDate', val, { shouldValidate: true, shouldDirty: true });
|
||||
if (val) {
|
||||
const d = new Date(val);
|
||||
d.setDate(d.getDate() + 7);
|
||||
setValue('dueDate', d.toISOString().split('T')[0], { shouldValidate: true, shouldDirty: true });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="dueDate">Due Date</Label>
|
||||
<Input id="dueDate" type="date" {...register('dueDate')} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Remarks */}
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="remarks">Remarks</Label>
|
||||
<Input id="remarks" {...register('remarks')} placeholder="Optional remarks" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">Description (Internal Note)</Label>
|
||||
<Textarea id="description" {...register('description')} rows={2} placeholder="Enter description..." />
|
||||
</div>
|
||||
|
||||
{/* Organizations */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-2">
|
||||
@@ -589,6 +514,81 @@ export function CorrespondenceForm({ initialData, uuid }: { initialData?: Initia
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Subject */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="subject">Subject *</Label>
|
||||
<Input id="subject" {...register('subject')} placeholder="Enter subject" />
|
||||
{errors.subject && <p className="text-sm text-destructive">{errors.subject.message}</p>}
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="body">Body (Content)</Label>
|
||||
<Textarea id="body" {...register('body')} rows={6} placeholder="Enter letter content..." />
|
||||
</div>
|
||||
|
||||
{/* Date Fields */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="documentDate">Document Date</Label>
|
||||
<Input
|
||||
id="documentDate"
|
||||
type="date"
|
||||
{...register('documentDate')}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
setValue('documentDate', val, { shouldValidate: true, shouldDirty: true });
|
||||
if (val) {
|
||||
setValue('issuedDate', val, { shouldValidate: true, shouldDirty: true });
|
||||
setValue('receivedDate', val, { shouldValidate: true, shouldDirty: true });
|
||||
const d = new Date(val);
|
||||
d.setDate(d.getDate() + 7);
|
||||
setValue('dueDate', d.toISOString().split('T')[0], { shouldValidate: true, shouldDirty: true });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="issuedDate">Issued Date</Label>
|
||||
<Input id="issuedDate" type="date" {...register('issuedDate')} />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="receivedDate">Received Date</Label>
|
||||
<Input
|
||||
id="receivedDate"
|
||||
type="date"
|
||||
{...register('receivedDate')}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
setValue('receivedDate', val, { shouldValidate: true, shouldDirty: true });
|
||||
if (val) {
|
||||
const d = new Date(val);
|
||||
d.setDate(d.getDate() + 7);
|
||||
setValue('dueDate', d.toISOString().split('T')[0], { shouldValidate: true, shouldDirty: true });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="dueDate">Due Date</Label>
|
||||
<Input id="dueDate" type="date" {...register('dueDate')} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Remarks */}
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="remarks">Remarks</Label>
|
||||
<Input id="remarks" {...register('remarks')} placeholder="Optional remarks" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description">Description (Internal Note)</Label>
|
||||
<Textarea id="description" {...register('description')} rows={2} placeholder="Enter description..." />
|
||||
</div>
|
||||
|
||||
{/* Importance */}
|
||||
<div className="space-y-2">
|
||||
<Label>Importance</Label>
|
||||
|
||||
@@ -345,27 +345,6 @@ export function RFAForm() {
|
||||
<h3 className="text-lg font-semibold mb-4">RFA Information</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="subject">Subject *</Label>
|
||||
<Input id="subject" {...register('subject')} placeholder="Enter subject" />
|
||||
{errors.subject && <p className="text-sm text-destructive mt-1">{errors.subject.message}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="body">Body (Content)</Label>
|
||||
<Textarea id="body" {...register('body')} rows={4} placeholder="Enter content..." />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="remarks">Remarks</Label>
|
||||
<Input id="remarks" {...register('remarks')} placeholder="Optional remarks" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Input id="description" {...register('description')} placeholder="Enter key description" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Project *</Label>
|
||||
<Select
|
||||
@@ -540,6 +519,28 @@ export function RFAForm() {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="subject">Subject *</Label>
|
||||
<Input id="subject" {...register('subject')} placeholder="Enter subject" />
|
||||
{errors.subject && <p className="text-sm text-destructive mt-1">{errors.subject.message}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="body">Body (Content)</Label>
|
||||
<Textarea id="body" {...register('body')} rows={4} placeholder="Enter content..." />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="remarks">Remarks</Label>
|
||||
<Input id="remarks" {...register('remarks')} placeholder="Optional remarks" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Input id="description" {...register('description')} placeholder="Enter key description" />
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
# LCBP3-DMS App Summary
|
||||
|
||||
## What It Is
|
||||
|
||||
LCBP3-DMS is a document management system for the Laem Chabang Port Phase 3 construction project. Repo docs describe it as a system for managing construction documents, approvals, workflows, and cross-organization communication. The codebase is split into a Next.js frontend and a NestJS backend, with MariaDB, Redis, and Elasticsearch in the application stack.
|
||||
|
||||
## Who It's For
|
||||
|
||||
Primary users are document-heavy construction project teams working across multiple organizations. Repo evidence points especially to Document Control, Org Admin, Engineer/Reviewer, Superadmin, consultants, supervisors, and contractors.
|
||||
|
||||
## What It Does
|
||||
|
||||
- Manages correspondence records between organizations.
|
||||
- Supports RFA workflows for technical approval requests.
|
||||
- Tracks contract and shop drawings.
|
||||
- Handles transmittals and circulation sheets.
|
||||
- Applies 4-level RBAC / CASL-based access control.
|
||||
- Generates document numbers with Redis-backed locking rules.
|
||||
- Supports file upload and attachment handling with cleanup jobs.
|
||||
- Exposes admin screens for users, orgs, projects, workflows, numbering, and audit logs.
|
||||
- Provides search through an Elasticsearch-backed search module.
|
||||
- Includes notification processing and a migration area for legacy document import.
|
||||
|
||||
\newpage
|
||||
|
||||
## How It Works
|
||||
|
||||
### Frontend
|
||||
|
||||
- `frontend/` is a Next.js 16 App Router app.
|
||||
- Route groups in `frontend/app/` show three main surfaces: `(auth)`, `(dashboard)`, and `(admin)`.
|
||||
- Installed frontend libraries indicate form validation and data fetching with React Hook Form, Zod, TanStack Query, and Zustand.
|
||||
- `frontend/app/api/auth/[...nextauth]/route.ts`: Not found in repo via direct read during this task, but the route file exists in the app tree.
|
||||
|
||||
### Backend
|
||||
|
||||
- `backend/src/main.ts` boots a NestJS 11 app with Helmet, CORS, 50 MB body limits, global validation, response transformation, exception filtering, and Swagger.
|
||||
- `backend/src/app.module.ts` wires Config, TypeORM for MariaDB, BullMQ, Redis, scheduling, throttling, logging, monitoring, resilience, and feature modules.
|
||||
- Feature modules present in code include auth, user, project, organization, contract, correspondence, RFA, drawing, transmittal, circulation, workflow-engine, document-numbering, search, notification, audit-log, dashboard, master, and migration.
|
||||
|
||||
### Data Flow
|
||||
|
||||
- Browser -> Next.js UI -> backend API under `/api`.
|
||||
- Backend validates requests, applies guards/interceptors, and persists entities to MariaDB through TypeORM.
|
||||
- Redis is used in the running architecture for BullMQ queues and Redis module integration; repo specs also tie Redis to locking, caching, and idempotency patterns.
|
||||
- Search requests flow through the NestJS search module into Elasticsearch.
|
||||
- Notification work is queued through BullMQ and also exposes a WebSocket gateway.
|
||||
- Migration services write through backend modules and the shared file-storage module rather than direct client-side data access.
|
||||
|
||||
## How To Run
|
||||
|
||||
1. Install workspace dependencies with `pnpm install` at repo root.
|
||||
2. Start local infra from [`backend/docker-compose.yml`](/E:/np-dms/lcbp3/backend/docker-compose.yml) to bring up MariaDB, Redis, and Elasticsearch.
|
||||
3. Prepare env files: `frontend/.env.example` -> `frontend/.env.local`; backend example env file: Not found in repo.
|
||||
4. Load the SQL schema and seed data using the commands documented in [`README.md`](/E:/np-dms/lcbp3/README.md).
|
||||
5. Run backend with `pnpm run start:dev` in `backend/`.
|
||||
6. Run frontend with `pnpm run dev` in `frontend/`.
|
||||
|
||||
## Notes
|
||||
|
||||
- Minimal dev URLs from repo docs: frontend `http://localhost:3000`, backend `http://localhost:3001`, Swagger `/docs`.
|
||||
- Exact production deployment topology is documented, but this summary keeps only the minimum local-start path.
|
||||
Reference in New Issue
Block a user