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' })
|
@ApiProperty({ example: 'URGENT', description: 'ชื่อ Tag' })
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
tag_name!: string; // เพิ่ม !
|
tagName!: string;
|
||||||
|
|
||||||
@ApiProperty({ example: 'คำอธิบาย', description: 'คำอธิบาย' })
|
@ApiProperty({ example: 'คำอธิบาย', description: 'คำอธิบาย' })
|
||||||
@IsString()
|
@IsString()
|
||||||
@@ -19,7 +19,7 @@ export class CreateTagDto {
|
|||||||
})
|
})
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
color_code?: string;
|
colorCode?: string;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
example: 1,
|
example: 1,
|
||||||
@@ -27,5 +27,5 @@ export class CreateTagDto {
|
|||||||
required: false,
|
required: false,
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
project_id?: number | string;
|
projectId?: number | string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,17 @@
|
|||||||
|
|
||||||
import { IsString, IsOptional, IsInt, Min } from 'class-validator';
|
import { IsString, IsOptional, IsInt, Min } from 'class-validator';
|
||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import { ApiPropertyOptional } from '@nestjs/swagger';
|
import { ApiPropertyOptional, ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
export class SearchTagDto {
|
export class SearchTagDto {
|
||||||
@ApiPropertyOptional({
|
@ApiProperty({
|
||||||
description: 'ID โครงการ (ใช้กรอง Tag ของแต่ละโปรเจกต์)',
|
example: 1,
|
||||||
|
description:
|
||||||
|
'Project ID or UUID (ใช้กรอง Tag ของแต่ละโปรเจกต์) - ADR-019: Accept UUID',
|
||||||
|
required: false,
|
||||||
})
|
})
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@Type(() => Number)
|
projectId?: number | string;
|
||||||
@IsInt()
|
|
||||||
project_id?: number;
|
|
||||||
|
|
||||||
@ApiPropertyOptional({ description: 'คำค้นหา (ชื่อ Tag หรือ คำอธิบาย)' })
|
@ApiPropertyOptional({ description: 'คำค้นหา (ชื่อ Tag หรือ คำอธิบาย)' })
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
|||||||
@@ -303,25 +303,24 @@ export class MasterService {
|
|||||||
.createQueryBuilder('tag')
|
.createQueryBuilder('tag')
|
||||||
.leftJoinAndSelect('tag.project', 'project');
|
.leftJoinAndSelect('tag.project', 'project');
|
||||||
|
|
||||||
if (query?.project_id) {
|
if (query?.projectId) {
|
||||||
// In Tags, we use project_id (INT) directly or resolve if UUID passed via query
|
|
||||||
const internalId = await this.uuidResolver.resolveProjectId(
|
const internalId = await this.uuidResolver.resolveProjectId(
|
||||||
query.project_id
|
query.projectId
|
||||||
);
|
);
|
||||||
qb.andWhere('tag.project_id = :projectId', {
|
qb.andWhere('tag.projectId = :projectId', {
|
||||||
projectId: internalId,
|
projectId: internalId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query?.search) {
|
if (query?.search) {
|
||||||
qb.andWhere(
|
qb.andWhere(
|
||||||
'(tag.tag_name LIKE :search OR tag.description LIKE :search)',
|
'(tag.tagName LIKE :search OR tag.description LIKE :search)',
|
||||||
{
|
{
|
||||||
search: `%${query.search}%`,
|
search: `%${query.search}%`,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
qb.orderBy('tag.tag_name', 'ASC');
|
qb.orderBy('tag.tagName', 'ASC');
|
||||||
if (query?.page && query?.limit) {
|
if (query?.page && query?.limit) {
|
||||||
const page = query.page;
|
const page = query.page;
|
||||||
const limit = query.limit;
|
const limit = query.limit;
|
||||||
@@ -342,11 +341,13 @@ export class MasterService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async createTag(dto: CreateTagDto, userId: number) {
|
async createTag(dto: CreateTagDto, userId: number) {
|
||||||
const internalProjectId = dto.project_id
|
const internalProjectId = dto.projectId
|
||||||
? await this.uuidResolver.resolveProjectId(dto.project_id)
|
? await this.uuidResolver.resolveProjectId(dto.projectId)
|
||||||
: null;
|
: null;
|
||||||
const tag = this.tagRepo.create({
|
const tag = this.tagRepo.create({
|
||||||
...dto,
|
tagName: dto.tagName,
|
||||||
|
colorCode: dto.colorCode,
|
||||||
|
description: dto.description,
|
||||||
projectId: internalProjectId,
|
projectId: internalProjectId,
|
||||||
createdBy: userId,
|
createdBy: userId,
|
||||||
});
|
});
|
||||||
@@ -355,10 +356,18 @@ export class MasterService {
|
|||||||
|
|
||||||
async updateTag(id: number, dto: UpdateTagDto) {
|
async updateTag(id: number, dto: UpdateTagDto) {
|
||||||
const tag = await this.findOneTag(id);
|
const tag = await this.findOneTag(id);
|
||||||
if (dto.project_id) {
|
let internalProjectId = dto.projectId;
|
||||||
dto.project_id = await this.uuidResolver.resolveProjectId(dto.project_id);
|
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);
|
return this.tagRepo.save(tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export default function TagsPage() {
|
|||||||
|
|
||||||
const columns: ColumnDef<Tag>[] = [
|
const columns: ColumnDef<Tag>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: 'project_id',
|
accessorKey: 'projectId',
|
||||||
header: 'Project',
|
header: 'Project',
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const item = row.original as Tag & { project?: { id?: number | string; publicId?: string; projectName?: string; projectCode?: string } };
|
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',
|
header: 'Tag Name',
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const color = String(row.original.colorCode || 'default');
|
const color = String(row.original.colorCode || 'default');
|
||||||
@@ -58,9 +58,9 @@ export default function TagsPage() {
|
|||||||
|
|
||||||
const formatPayload = (data: Record<string, unknown>) => {
|
const formatPayload = (data: Record<string, unknown>) => {
|
||||||
const payload = { ...data };
|
const payload = { ...data };
|
||||||
// ADR-019: project_id is now a UUID string or '__none__' for global
|
// ADR-019: projectId is now a UUID string or '__none__' for global
|
||||||
if (!payload.project_id || payload.project_id === '__none__') {
|
if (!payload.projectId || payload.projectId === '__none__') {
|
||||||
payload.project_id = null;
|
payload.projectId = null;
|
||||||
}
|
}
|
||||||
return payload;
|
return payload;
|
||||||
};
|
};
|
||||||
@@ -73,12 +73,12 @@ export default function TagsPage() {
|
|||||||
queryKey={['tags']}
|
queryKey={['tags']}
|
||||||
fetchFn={async () => {
|
fetchFn={async () => {
|
||||||
const items = await masterDataService.getTags();
|
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) => {
|
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 {
|
return {
|
||||||
...item,
|
...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;
|
} as Tag;
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -90,7 +90,7 @@ export default function TagsPage() {
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
fields={[
|
fields={[
|
||||||
{
|
{
|
||||||
name: 'project_id',
|
name: 'projectId',
|
||||||
label: 'Project Scope',
|
label: 'Project Scope',
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: projectOptions,
|
options: projectOptions,
|
||||||
|
|||||||
@@ -439,81 +439,6 @@ export function CorrespondenceForm({ initialData, uuid }: { initialData?: Initia
|
|||||||
</div>
|
</div>
|
||||||
</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 */}
|
{/* Organizations */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -589,6 +514,81 @@ export function CorrespondenceForm({ initialData, uuid }: { initialData?: Initia
|
|||||||
</div>
|
</div>
|
||||||
</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 */}
|
{/* Importance */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Importance</Label>
|
<Label>Importance</Label>
|
||||||
|
|||||||
@@ -345,28 +345,7 @@ export function RFAForm() {
|
|||||||
<h3 className="text-lg font-semibold mb-4">RFA Information</h3>
|
<h3 className="text-lg font-semibold mb-4">RFA Information</h3>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<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>
|
|
||||||
<Label>Project *</Label>
|
<Label>Project *</Label>
|
||||||
<Select
|
<Select
|
||||||
value={selectedProjectId || undefined}
|
value={selectedProjectId || undefined}
|
||||||
@@ -540,6 +519,28 @@ export function RFAForm() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</Card>
|
</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