260321:1700 Correct Coresspondence / Doing RFA

This commit is contained in:
admin
2026-03-21 17:00:41 +07:00
parent dcf55f4d08
commit 03d16cfd64
57 changed files with 1923 additions and 663 deletions
+47 -6
View File
@@ -24,8 +24,10 @@ export interface NumberingTemplate {
uuid?: string;
};
formatTemplate: string;
disciplineId: number;
description?: string;
resetSequenceYearly: boolean; // Controls yearly counter reset
isActive: number;
createdAt?: string;
updatedAt?: string;
}
@@ -38,8 +40,10 @@ export interface SaveTemplateDto {
projectId: number | string;
correspondenceTypeId: number | null;
formatTemplate: string;
disciplineId?: number;
description?: string;
resetSequenceYearly?: boolean;
isActive?: number;
}
/**
@@ -151,10 +155,22 @@ export const numberingApi = {
/**
* Save (create or update) a template
*/
saveTemplate: async (dto: Partial<NumberingTemplate>): Promise<NumberingTemplate> => {
saveTemplate: async (dto: SaveTemplateDto): Promise<NumberingTemplate> => {
// Clean the DTO to avoid sending nested objects that might confuse TypeORM or violate constraints
const cleanDto: any = {
id: dto.id,
projectId: dto.projectId,
correspondenceTypeId: dto.correspondenceTypeId,
disciplineId: dto.disciplineId || 0,
formatTemplate: dto.formatTemplate,
description: dto.description,
resetSequenceYearly: dto.resetSequenceYearly,
isActive: dto.isActive ?? 1,
};
const res = await apiClient.post<any>(
'/admin/document-numbering/templates',
dto
cleanDto
);
return res.data.data || res.data;
},
@@ -281,13 +297,38 @@ export const numberingApi = {
subTypeId?: number;
rfaTypeId?: number;
recipientOrganizationId?: number | string;
}): Promise<{ previewNumber: string; nextSequence: number }> => {
const res = await apiClient.post<{ data: { previewNumber: string; nextSequence: number } }>(
}): Promise<{ previewNumber: string; nextSequence: number; isDefault: boolean }> => {
const res = await apiClient.post<any>(
'/document-numbering/preview',
ctx
);
// Backend wraps response in { data: { ... }, message: "Success" }
return res.data.data || res.data;
// Explicit debug log for frontend developers to see in browser console
console.log("[numberingApi.previewNumber] Raw Response Data:", res.data);
const body = res.data;
console.log("[numberingApi.previewNumber] Full Body:", body);
// Drill down to find the actual data object
let data = body;
let depth = 0;
while (data && typeof data === 'object' && !data.previewNumber && !data.number && data.data && depth < 3) {
data = data.data;
depth++;
}
console.log(`[numberingApi.previewNumber] Unwrapped at depth ${depth}:`, data);
// Final extraction
const previewNumber = data?.previewNumber || data?.number || (typeof data === 'string' ? data : '');
const nextSequence = data?.nextSequence ?? data?.sequence ?? 0;
const isDefault = data?.isDefault === true;
return {
previewNumber: previewNumber || JSON.stringify(body), // Fallback to body string if all else fails
nextSequence: nextSequence,
isDefault: isDefault
};
},
/**
+61 -5
View File
@@ -23,6 +23,59 @@ function getJwtExpiry(token: string): number {
}
}
interface TokenPayload {
access_token: string;
refresh_token?: string;
}
interface LoginPayload extends TokenPayload {
user: {
user_id: number;
username: string;
email?: string;
firstName?: string;
lastName?: string;
role?: string;
primaryOrganizationId?: number;
};
}
function unwrapApiResponse(value: unknown): unknown {
let current = value;
for (let i = 0; i < 5; i += 1) {
if (!current || typeof current !== "object") {
return current;
}
const record = current as Record<string, unknown>;
if (typeof record.access_token === "string") {
return current;
}
if (!("data" in record)) {
return current;
}
current = record.data;
}
return current;
}
function isTokenPayload(value: unknown): value is TokenPayload {
return !!value && typeof value === "object" && typeof (value as Record<string, unknown>).access_token === "string";
}
function isLoginPayload(value: unknown): value is LoginPayload {
if (!isTokenPayload(value)) {
return false;
}
const user = (value as unknown as { user?: unknown }).user;
return !!user && typeof user === "object" && typeof (user as Record<string, unknown>).username === "string";
}
async function refreshAccessToken(token: JWT) {
try {
const response = await fetch(`${baseUrl}/auth/refresh`, {
@@ -38,7 +91,11 @@ async function refreshAccessToken(token: JWT) {
throw refreshedTokens;
}
const data = refreshedTokens.data || refreshedTokens;
const data = unwrapApiResponse(refreshedTokens);
if (!isTokenPayload(data)) {
throw new Error("Invalid refresh response format");
}
return {
...token,
@@ -100,10 +157,9 @@ export const {
}
const data = await res.json();
// Handling both { data: { ... } } and direct { ... } response formats
const backendData = data.data || data;
const backendData = unwrapApiResponse(data);
if (!backendData || !backendData.access_token) {
if (!isLoginPayload(backendData)) {
console.error("[AUTH] Login failed: Invalid response format from backend (missing access_token)");
return null;
}
@@ -112,7 +168,7 @@ export const {
return {
id: backendData.user.user_id.toString(),
name: `${backendData.user.firstName} ${backendData.user.lastName}`,
name: `${backendData.user.firstName ?? ""} ${backendData.user.lastName ?? ""}`.trim(),
email: backendData.user.email,
username: backendData.user.username,
role: backendData.user.role || "User",
+26 -8
View File
@@ -15,6 +15,24 @@ import {
SearchOrganizationDto,
} from "@/types/dto/organization/organization.dto";
const extractArrayData = <T>(value: unknown): T[] => {
let current: unknown = value;
for (let i = 0; i < 5; i += 1) {
if (Array.isArray(current)) {
return current as T[];
}
if (!current || typeof current !== "object" || !("data" in current)) {
return [];
}
current = (current as { data?: unknown }).data;
}
return Array.isArray(current) ? (current as T[]) : [];
};
export const masterDataService = {
// --- Tags Management ---
@@ -77,7 +95,7 @@ export const masterDataService = {
return response.data;
},
/** แก้ไองค์กร */
/** แก้ไองค์กร */
updateOrganization: async (uuid: string, data: UpdateOrganizationDto) => {
const response = await apiClient.put(`/organizations/${uuid}`, data);
return response.data;
@@ -97,7 +115,7 @@ export const masterDataService = {
const response = await apiClient.get("/master/disciplines", {
params: { contractId }
});
return response.data.data || response.data;
return extractArrayData(response.data);
},
/** สร้างสาขางานใหม่ */
@@ -119,7 +137,7 @@ export const masterDataService = {
const response = await apiClient.get("/master/sub-types", {
params: { contractId, correspondenceTypeId: typeId }
});
return response.data.data || response.data;
return extractArrayData(response.data);
},
/** สร้างประเภทย่อยใหม่ */
@@ -135,7 +153,7 @@ export const masterDataService = {
const response = await apiClient.get("/master/rfa-types", {
params: { contractId }
});
return response.data.data || response.data;
return extractArrayData(response.data);
},
/** สร้างประเภท RFA ใหม่ */
@@ -156,7 +174,7 @@ export const masterDataService = {
// --- Correspondence Types Management ---
getCorrespondenceTypes: async () => {
const response = await apiClient.get("/master/correspondence-types");
return response.data.data || response.data;
return extractArrayData(response.data);
},
createCorrespondenceType: async (data: CreateCorrespondenceTypeDto) => {
@@ -191,18 +209,18 @@ export const masterDataService = {
const response = await apiClient.get("/drawings/contract/categories", {
params: { projectId }
});
return response.data.data || response.data;
return extractArrayData(response.data);
},
getShopMainCategories: async (projectId: number) => {
const response = await apiClient.get("/drawings/shop/main-categories", { params: { projectId } });
return response.data.data || response.data;
return extractArrayData(response.data);
},
getShopSubCategories: async (projectId: number, mainCategoryId?: number) => {
const response = await apiClient.get("/drawings/shop/sub-categories", {
params: { projectId, mainCategoryId }
});
return response.data.data || response.data;
return extractArrayData(response.data);
}
};
+65 -3
View File
@@ -7,6 +7,68 @@ import {
CommitBatchDto,
} from '@/types/migration';
interface WrappedData {
data?: unknown;
}
const extractNestedData = <T,>(value: unknown): T => {
let current: unknown = value;
for (let i = 0; i < 5; i += 1) {
if (!current || typeof current !== 'object' || !('data' in current)) {
return current as T;
}
current = (current as WrappedData).data;
}
return current as T;
};
const normalizePaginatedResponse = <T,>(value: unknown): PaginatedResponse<T> => {
const extracted = extractNestedData<unknown>(value);
if (!extracted || typeof extracted !== 'object') {
return {
items: [],
total: 0,
page: 1,
limit: 0,
totalPages: 0,
};
}
const response = extracted as Partial<PaginatedResponse<T>> & { data?: unknown };
if (Array.isArray(response.items)) {
return {
items: response.items,
total: response.total ?? response.items.length,
page: response.page ?? 1,
limit: response.limit ?? response.items.length,
totalPages: response.totalPages ?? 1,
};
}
if (Array.isArray(response.data)) {
return {
items: response.data as T[],
total: response.total ?? response.data.length,
page: response.page ?? 1,
limit: response.limit ?? response.data.length,
totalPages: response.totalPages ?? 1,
};
}
return {
items: [],
total: 0,
page: 1,
limit: 0,
totalPages: 0,
};
};
export const migrationService = {
getReviewQueue: async (params: {
page?: number;
@@ -14,12 +76,12 @@ export const migrationService = {
status?: MigrationReviewStatus;
}): Promise<PaginatedResponse<MigrationReviewQueueItem>> => {
const { data } = await api.get('/migration/queue', { params });
return data?.data || data;
return normalizePaginatedResponse<MigrationReviewQueueItem>(data);
},
getQueueItem: async (id: number): Promise<MigrationReviewQueueItem> => {
const { data } = await api.get(`/migration/queue/${id}`);
return data?.data || data;
return extractNestedData<MigrationReviewQueueItem>(data);
},
getErrors: async (params: {
@@ -27,7 +89,7 @@ export const migrationService = {
limit?: number;
}): Promise<PaginatedResponse<MigrationErrorItem>> => {
const { data } = await api.get('/migration/errors', { params });
return data?.data || data;
return normalizePaginatedResponse<MigrationErrorItem>(data);
},
approveQueueItem: async (id: number, payload: any, idempotencyKey: string) => {
+27 -4
View File
@@ -1,7 +1,7 @@
import apiClient from '@/lib/api/client';
export interface Session {
id: string; // tokenId
id: number;
userId: number;
user: {
username: string;
@@ -14,10 +14,33 @@ export interface Session {
isCurrent: boolean;
}
const extractArrayData = <T,>(value: unknown): T[] => {
let current: unknown = value;
for (let i = 0; i < 5; i += 1) {
if (Array.isArray(current)) {
return current as T[];
}
if (!current || typeof current !== 'object' || !('data' in current)) {
return [];
}
current = (current as { data?: unknown }).data;
}
return Array.isArray(current) ? (current as T[]) : [];
};
const transformSession = (session: Session | (Omit<Session, 'id'> & { id: string | number })): Session => ({
...session,
id: typeof session.id === 'number' ? session.id : Number(session.id),
});
export const sessionService = {
getActiveSessions: async () => {
const response = await apiClient.get<Session[] | { data: Session[] }>('/auth/sessions');
return (response.data as { data: Session[] }).data ?? response.data;
getActiveSessions: async (): Promise<Session[]> => {
const response = await apiClient.get<Session[] | { data: Session[] } | { data: { data: Session[] } }>('/auth/sessions');
return extractArrayData<Session | (Omit<Session, 'id'> & { id: string | number })>(response.data).map(transformSession);
},
revokeSession: async (sessionId: number) => {
+22 -18
View File
@@ -1,5 +1,5 @@
import apiClient from "@/lib/api/client";
import { CreateUserDto, UpdateUserDto, SearchUserDto, User } from "@/types/user";
import { CreateUserDto, UpdateUserDto, SearchUserDto, User, Role } from "@/types/user";
/** Raw API user shape (before transform) */
interface RawUser {
@@ -9,6 +9,24 @@ interface RawUser {
[key: string]: unknown;
}
const extractArrayData = <T,>(value: unknown): T[] => {
let current: unknown = value;
for (let i = 0; i < 5; i += 1) {
if (Array.isArray(current)) {
return current as T[];
}
if (!current || typeof current !== "object" || !("data" in current)) {
return [];
}
current = (current as { data?: unknown }).data;
}
return Array.isArray(current) ? (current as T[]) : [];
};
const transformUser = (user: RawUser): User => {
return {
...(user as unknown as User),
@@ -24,26 +42,12 @@ type UserListResponse = User[] | { data: User[] | { data: User[] } };
export const userService = {
getAll: async (params?: SearchUserDto) => {
const response = await apiClient.get<UserListResponse>("/users", { params });
// Handle both paginated and non-paginated responses
let rawData: RawUser[] | unknown = response.data;
if (rawData && !Array.isArray(rawData) && 'data' in (rawData as object)) {
rawData = (rawData as { data: unknown }).data;
}
if (rawData && !Array.isArray(rawData) && typeof rawData === 'object' && 'data' in (rawData as object)) {
rawData = (rawData as { data: unknown }).data;
}
if (!Array.isArray(rawData)) return [];
return (rawData as RawUser[]).map(transformUser);
return extractArrayData<RawUser>(response.data).map(transformUser);
},
getRoles: async () => {
getRoles: async (): Promise<Role[]> => {
const response = await apiClient.get<{ data: unknown } | unknown>("/users/roles");
if (response.data && typeof response.data === 'object' && 'data' in (response.data as object)) {
return (response.data as { data: unknown }).data;
}
return response.data;
return extractArrayData<Role>(response.data);
},
getByUuid: async (uuid: string) => {
+104 -11
View File
@@ -7,18 +7,112 @@ import {
GetAvailableActionsDto,
} from '@/types/dto/workflow-engine/workflow-engine.dto';
import { Workflow } from '@/types/workflow';
import { Workflow, WorkflowType } from '@/types/workflow';
const mapWorkflow = (backendObj: any): Workflow => {
interface WorkflowResponseShape {
data?: unknown;
}
interface WorkflowDslShape {
workflowName?: string;
description?: string;
dslDefinition?: string;
workflow?: string;
states?: unknown;
}
interface BackendWorkflowShape {
id?: string | number;
workflow_code?: string;
description?: string;
version?: number;
is_active?: boolean;
dsl?: string | WorkflowDslShape;
compiled?: {
states?: Record<string, unknown>;
};
updated_at?: string;
}
const extractArrayData = <T,>(value: unknown): T[] => {
let current: unknown = value;
for (let i = 0; i < 5; i += 1) {
if (Array.isArray(current)) {
return current as T[];
}
if (!current || typeof current !== 'object' || !('data' in current)) {
return [];
}
current = (current as { data?: unknown }).data;
}
return Array.isArray(current) ? (current as T[]) : [];
};
const extractNestedData = <T,>(value: unknown): T => {
let current: unknown = value;
for (let i = 0; i < 5; i += 1) {
if (!current || typeof current !== 'object' || !('data' in current)) {
return current as T;
}
current = (current as WorkflowResponseShape).data;
}
return current as T;
};
const extractDslDefinition = (dsl: BackendWorkflowShape['dsl']): string => {
if (typeof dsl === 'string') {
return dsl;
}
if (!dsl || typeof dsl !== 'object') {
return '';
}
if (typeof dsl.dslDefinition === 'string') {
return dsl.dslDefinition;
}
return JSON.stringify(dsl, null, 2);
};
const normalizeWorkflowType = (workflowCode?: string): WorkflowType => {
const normalizedCode = workflowCode?.toUpperCase() ?? '';
if (normalizedCode.includes('RFA')) {
return 'RFA';
}
if (normalizedCode.includes('DRAWING')) {
return 'DRAWING';
}
return 'CORRESPONDENCE';
};
const mapWorkflow = (backendObj: BackendWorkflowShape): Workflow => {
if (!backendObj) throw new Error('Workflow not found');
return {
workflowId: backendObj.id,
workflowName: backendObj.dsl?.workflowName || backendObj.workflow_code,
description: backendObj.description || backendObj.dsl?.description || '',
workflowType: backendObj.workflow_code?.toUpperCase() || backendObj.workflow_code,
workflowId: backendObj.id ?? backendObj.workflow_code ?? '',
workflowName:
(typeof backendObj.dsl === 'object' ? backendObj.dsl?.workflowName : undefined) ||
(typeof backendObj.dsl === 'object' ? backendObj.dsl?.workflow : undefined) ||
backendObj.workflow_code ||
'',
description:
backendObj.description ||
(typeof backendObj.dsl === 'object' ? backendObj.dsl?.description : undefined) ||
'',
workflowType: normalizeWorkflowType(backendObj.workflow_code),
version: backendObj.version || 1,
isActive: backendObj.is_active,
dslDefinition: typeof backendObj.dsl === 'string' ? backendObj.dsl : backendObj.dsl?.dslDefinition || JSON.stringify(backendObj.dsl, null, 2),
isActive: backendObj.is_active ?? false,
dslDefinition: extractDslDefinition(backendObj.dsl),
stepCount: backendObj.compiled?.states ? Object.keys(backendObj.compiled.states).length : 0,
updatedAt: backendObj.updated_at || new Date().toISOString(),
};
@@ -53,8 +147,7 @@ export const workflowEngineService = {
*/
getDefinitions: async (): Promise<Workflow[]> => {
const response = await apiClient.get('/workflow-engine/definitions');
const data = response.data?.data || response.data;
return Array.isArray(data) ? data.map(mapWorkflow) : data;
return extractArrayData<BackendWorkflowShape>(response.data).map((workflow) => mapWorkflow(workflow));
},
/**
@@ -63,7 +156,7 @@ export const workflowEngineService = {
*/
getDefinitionById: async (id: string | number): Promise<Workflow> => {
const response = await apiClient.get(`/workflow-engine/definitions/${id}`);
const data = response.data?.data || response.data;
const data = extractNestedData<BackendWorkflowShape>(response.data);
return mapWorkflow(data);
},