251205:0000 Just start debug backend/frontend

This commit is contained in:
2025-12-05 00:32:02 +07:00
parent dc8b80c5f9
commit 2865bebdb1
88 changed files with 6751 additions and 1016 deletions

103
frontend/lib/api/admin.ts Normal file
View File

@@ -0,0 +1,103 @@
import { User, CreateUserDto, Organization, AuditLog } from "@/types/admin";
// Mock Data
const mockUsers: User[] = [
{
user_id: 1,
username: "admin",
email: "admin@example.com",
first_name: "System",
last_name: "Admin",
is_active: true,
roles: [{ role_id: 1, role_name: "ADMIN", description: "Administrator" }],
},
{
user_id: 2,
username: "jdoe",
email: "john.doe@example.com",
first_name: "John",
last_name: "Doe",
is_active: true,
roles: [{ role_id: 2, role_name: "USER", description: "Regular User" }],
},
];
const mockOrgs: Organization[] = [
{
org_id: 1,
org_code: "PAT",
org_name: "Port Authority of Thailand",
org_name_th: "การท่าเรือแห่งประเทศไทย",
description: "Owner",
},
{
org_id: 2,
org_code: "CNPC",
org_name: "CNPC Consortium",
description: "Main Contractor",
},
];
const mockLogs: AuditLog[] = [
{
audit_log_id: 1,
user_name: "admin",
action: "CREATE",
entity_type: "user",
description: "Created user 'jdoe'",
ip_address: "192.168.1.1",
created_at: new Date(Date.now() - 1000 * 60 * 60 * 24).toISOString(),
},
{
audit_log_id: 2,
user_name: "jdoe",
action: "UPDATE",
entity_type: "rfa",
description: "Updated status of RFA-001 to APPROVED",
ip_address: "192.168.1.5",
created_at: new Date(Date.now() - 1000 * 60 * 30).toISOString(),
},
];
export const adminApi = {
getUsers: async (): Promise<User[]> => {
await new Promise((resolve) => setTimeout(resolve, 500));
return [...mockUsers];
},
createUser: async (data: CreateUserDto): Promise<User> => {
await new Promise((resolve) => setTimeout(resolve, 800));
const newUser: User = {
user_id: Math.max(...mockUsers.map((u) => u.user_id)) + 1,
username: data.username,
email: data.email,
first_name: data.first_name,
last_name: data.last_name,
is_active: data.is_active,
roles: data.roles.map((id) => ({
role_id: id,
role_name: id === 1 ? "ADMIN" : "USER",
description: "",
})),
};
mockUsers.push(newUser);
return newUser;
},
getOrganizations: async (): Promise<Organization[]> => {
await new Promise((resolve) => setTimeout(resolve, 500));
return [...mockOrgs];
},
createOrganization: async (data: Omit<Organization, "org_id">): Promise<Organization> => {
await new Promise((resolve) => setTimeout(resolve, 600));
const newOrg = { ...data, org_id: Math.max(...mockOrgs.map((o) => o.org_id)) + 1 };
mockOrgs.push(newOrg);
return newOrg;
},
getAuditLogs: async (): Promise<AuditLog[]> => {
await new Promise((resolve) => setTimeout(resolve, 400));
return [...mockLogs];
},
};

View File

@@ -0,0 +1,85 @@
import { Correspondence, CreateCorrespondenceDto } from "@/types/correspondence";
// Mock Data
const mockCorrespondences: Correspondence[] = [
{
correspondence_id: 1,
document_number: "LCBP3-COR-001",
subject: "Submission of Monthly Report - Jan 2025",
description: "Please find attached the monthly progress report.",
status: "PENDING",
importance: "NORMAL",
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
from_organization_id: 1,
to_organization_id: 2,
document_type_id: 1,
from_organization: { id: 1, org_name: "Contractor A", org_code: "CON-A" },
to_organization: { id: 2, org_name: "Owner", org_code: "OWN" },
},
{
correspondence_id: 2,
document_number: "LCBP3-COR-002",
subject: "Request for Information regarding Foundation",
description: "Clarification needed on drawing A-101.",
status: "IN_REVIEW",
importance: "HIGH",
created_at: new Date(Date.now() - 86400000).toISOString(),
updated_at: new Date(Date.now() - 86400000).toISOString(),
from_organization_id: 2,
to_organization_id: 1,
document_type_id: 1,
from_organization: { id: 2, org_name: "Owner", org_code: "OWN" },
to_organization: { id: 1, org_name: "Contractor A", org_code: "CON-A" },
},
];
export const correspondenceApi = {
getAll: async (params?: { page?: number; status?: string; search?: string }) => {
// Simulate API delay
await new Promise((resolve) => setTimeout(resolve, 500));
let filtered = [...mockCorrespondences];
if (params?.status) {
filtered = filtered.filter((c) => c.status === params.status);
}
if (params?.search) {
const lowerSearch = params.search.toLowerCase();
filtered = filtered.filter((c) =>
c.subject.toLowerCase().includes(lowerSearch) ||
c.document_number.toLowerCase().includes(lowerSearch)
);
}
return {
items: filtered,
total: filtered.length,
page: params?.page || 1,
totalPages: 1,
};
},
getById: async (id: number) => {
await new Promise((resolve) => setTimeout(resolve, 500));
return mockCorrespondences.find((c) => c.correspondence_id === id);
},
create: async (data: CreateCorrespondenceDto) => {
await new Promise((resolve) => setTimeout(resolve, 1000));
const newId = Math.max(...mockCorrespondences.map((c) => c.correspondence_id)) + 1;
const newCorrespondence: Correspondence = {
correspondence_id: newId,
document_number: `LCBP3-COR-00${newId}`,
...data,
status: "DRAFT",
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
// Mock organizations for display
from_organization: { id: data.from_organization_id, org_name: "Mock Org From", org_code: "MOCK" },
to_organization: { id: data.to_organization_id, org_name: "Mock Org To", org_code: "MOCK" },
} as Correspondence; // Casting for simplicity in mock
mockCorrespondences.unshift(newCorrespondence);
return newCorrespondence;
},
};

View File

@@ -0,0 +1,65 @@
import { DashboardStats, ActivityLog, PendingTask } from "@/types/dashboard";
export const dashboardApi = {
getStats: async (): Promise<DashboardStats> => {
await new Promise((resolve) => setTimeout(resolve, 500));
return {
correspondences: 124,
rfas: 45,
approved: 89,
pending: 12,
};
},
getRecentActivity: async (): Promise<ActivityLog[]> => {
await new Promise((resolve) => setTimeout(resolve, 600));
return [
{
id: 1,
user: { name: "John Doe", initials: "JD" },
action: "Created RFA",
description: "RFA-001: Concrete Pouring Request",
createdAt: new Date(Date.now() - 1000 * 60 * 30).toISOString(), // 30 mins ago
targetUrl: "/rfas/1",
},
{
id: 2,
user: { name: "Jane Smith", initials: "JS" },
action: "Approved Correspondence",
description: "COR-005: Site Safety Report",
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 2).toISOString(), // 2 hours ago
targetUrl: "/correspondences/5",
},
{
id: 3,
user: { name: "Mike Johnson", initials: "MJ" },
action: "Uploaded Drawing",
description: "A-101: Ground Floor Plan Rev B",
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 5).toISOString(), // 5 hours ago
targetUrl: "/drawings/1",
},
];
},
getPendingTasks: async (): Promise<PendingTask[]> => {
await new Promise((resolve) => setTimeout(resolve, 400));
return [
{
id: 1,
title: "Review RFA-002",
description: "Approval required for steel reinforcement",
daysOverdue: 2,
url: "/rfas/2",
priority: "HIGH",
},
{
id: 2,
title: "Approve Monthly Report",
description: "January 2025 Progress Report",
daysOverdue: 0,
url: "/correspondences/10",
priority: "MEDIUM",
},
];
},
};

View File

@@ -0,0 +1,119 @@
import { Drawing, CreateDrawingDto, DrawingRevision } from "@/types/drawing";
// Mock Data
const mockDrawings: Drawing[] = [
{
drawing_id: 1,
drawing_number: "A-101",
title: "Ground Floor Plan",
type: "CONTRACT",
discipline_id: 2,
discipline: { id: 2, discipline_code: "ARC", discipline_name: "Architecture" },
sheet_number: "01",
scale: "1:100",
current_revision: "0",
issue_date: new Date(Date.now() - 100000000).toISOString(),
revision_count: 1,
revisions: [
{
revision_id: 1,
revision_number: "0",
revision_date: new Date(Date.now() - 100000000).toISOString(),
revision_description: "Issued for Construction",
revised_by_name: "John Doe",
file_url: "/mock-drawing.pdf",
is_current: true,
},
],
},
{
drawing_id: 2,
drawing_number: "S-201",
title: "Foundation Details",
type: "SHOP",
discipline_id: 1,
discipline: { id: 1, discipline_code: "STR", discipline_name: "Structure" },
sheet_number: "05",
scale: "1:50",
current_revision: "B",
issue_date: new Date().toISOString(),
revision_count: 2,
revisions: [
{
revision_id: 3,
revision_number: "B",
revision_date: new Date().toISOString(),
revision_description: "Updated reinforcement",
revised_by_name: "Jane Smith",
file_url: "/mock-drawing-v2.pdf",
is_current: true,
},
{
revision_id: 2,
revision_number: "A",
revision_date: new Date(Date.now() - 50000000).toISOString(),
revision_description: "First Submission",
revised_by_name: "Jane Smith",
file_url: "/mock-drawing-v1.pdf",
is_current: false,
},
],
},
];
export const drawingApi = {
getAll: async (params?: { type?: "CONTRACT" | "SHOP"; search?: string }) => {
await new Promise((resolve) => setTimeout(resolve, 500));
let filtered = [...mockDrawings];
if (params?.type) {
filtered = filtered.filter((d) => d.type === params.type);
}
if (params?.search) {
const lowerSearch = params.search.toLowerCase();
filtered = filtered.filter((d) =>
d.drawing_number.toLowerCase().includes(lowerSearch) ||
d.title.toLowerCase().includes(lowerSearch)
);
}
return filtered;
},
getById: async (id: number) => {
await new Promise((resolve) => setTimeout(resolve, 500));
return mockDrawings.find((d) => d.drawing_id === id);
},
create: async (data: CreateDrawingDto) => {
await new Promise((resolve) => setTimeout(resolve, 1000));
const newId = Math.max(...mockDrawings.map((d) => d.drawing_id)) + 1;
const newDrawing: Drawing = {
drawing_id: newId,
drawing_number: data.drawing_number,
title: data.title,
type: data.drawing_type,
discipline_id: data.discipline_id,
discipline: { id: data.discipline_id, discipline_code: "MOCK", discipline_name: "Mock Discipline" },
sheet_number: data.sheet_number,
scale: data.scale,
current_revision: "0",
issue_date: new Date().toISOString(),
revision_count: 1,
revisions: [
{
revision_id: newId * 10,
revision_number: "0",
revision_date: new Date().toISOString(),
revision_description: "Initial Upload",
revised_by_name: "Current User",
file_url: "#",
is_current: true,
}
]
};
mockDrawings.unshift(newDrawing);
return newDrawing;
},
};

View File

@@ -0,0 +1,50 @@
import { NotificationResponse } from "@/types/notification";
// Mock Data
let mockNotifications = [
{
notification_id: 1,
title: "RFA Approved",
message: "RFA-001 has been approved by the Project Manager.",
type: "SUCCESS" as const,
is_read: false,
created_at: new Date(Date.now() - 1000 * 60 * 15).toISOString(), // 15 mins ago
link: "/rfas/1",
},
{
notification_id: 2,
title: "New Correspondence",
message: "You have received a new correspondence from Contractor A.",
type: "INFO" as const,
is_read: false,
created_at: new Date(Date.now() - 1000 * 60 * 60).toISOString(), // 1 hour ago
link: "/correspondences/3",
},
{
notification_id: 3,
title: "Drawing Revision Required",
message: "Drawing S-201 requires revision based on recent comments.",
type: "WARNING" as const,
is_read: true,
created_at: new Date(Date.now() - 1000 * 60 * 60 * 24).toISOString(), // 1 day ago
link: "/drawings/2",
},
];
export const notificationApi = {
getUnread: async (): Promise<NotificationResponse> => {
await new Promise((resolve) => setTimeout(resolve, 300));
const unread = mockNotifications.filter((n) => !n.is_read);
return {
items: mockNotifications, // Return all for the list, but count unread
unreadCount: unread.length,
};
},
markAsRead: async (id: number) => {
await new Promise((resolve) => setTimeout(resolve, 200));
mockNotifications = mockNotifications.map((n) =>
n.notification_id === id ? { ...n, is_read: true } : n
);
},
};

View File

@@ -0,0 +1,111 @@
import { NumberingTemplate, NumberingSequence, CreateTemplateDto, TestGenerationResult } from "@/types/numbering";
// Mock Data
let mockTemplates: NumberingTemplate[] = [
{
template_id: 1,
document_type_id: "correspondence",
document_type_name: "Correspondence",
discipline_code: "",
template_format: "{ORG}-CORR-{YYYY}-{SEQ}",
example_number: "PAT-CORR-2025-0001",
current_number: 125,
reset_annually: true,
padding_length: 4,
is_active: true,
updated_at: new Date().toISOString(),
},
{
template_id: 2,
document_type_id: "rfa",
document_type_name: "RFA",
discipline_code: "STR",
template_format: "{ORG}-RFA-STR-{YYYY}-{SEQ}",
example_number: "ITD-RFA-STR-2025-0042",
current_number: 42,
reset_annually: true,
padding_length: 4,
is_active: true,
updated_at: new Date(Date.now() - 86400000).toISOString(),
},
];
const mockSequences: NumberingSequence[] = [
{
sequence_id: 1,
template_id: 1,
year: 2025,
organization_code: "PAT",
current_number: 125,
last_generated_number: "PAT-CORR-2025-0125",
updated_at: new Date().toISOString(),
},
{
sequence_id: 2,
template_id: 2,
year: 2025,
organization_code: "ITD",
discipline_code: "STR",
current_number: 42,
last_generated_number: "ITD-RFA-STR-2025-0042",
updated_at: new Date().toISOString(),
},
];
export const numberingApi = {
getTemplates: async (): Promise<NumberingTemplate[]> => {
await new Promise((resolve) => setTimeout(resolve, 500));
return [...mockTemplates];
},
getTemplate: async (id: number): Promise<NumberingTemplate | undefined> => {
await new Promise((resolve) => setTimeout(resolve, 300));
return mockTemplates.find((t) => t.template_id === id);
},
createTemplate: async (data: CreateTemplateDto): Promise<NumberingTemplate> => {
await new Promise((resolve) => setTimeout(resolve, 800));
const newTemplate: NumberingTemplate = {
template_id: Math.max(...mockTemplates.map((t) => t.template_id)) + 1,
document_type_name: data.document_type_id.toUpperCase(), // Simplified
...data,
example_number: "TEST-0001", // Simplified
current_number: data.starting_number - 1,
is_active: true,
updated_at: new Date().toISOString(),
};
mockTemplates.push(newTemplate);
return newTemplate;
},
updateTemplate: async (id: number, data: Partial<CreateTemplateDto>): Promise<NumberingTemplate> => {
await new Promise((resolve) => setTimeout(resolve, 600));
const index = mockTemplates.findIndex((t) => t.template_id === id);
if (index === -1) throw new Error("Template not found");
const updatedTemplate = { ...mockTemplates[index], ...data, updated_at: new Date().toISOString() };
mockTemplates[index] = updatedTemplate;
return updatedTemplate;
},
getSequences: async (templateId: number): Promise<NumberingSequence[]> => {
await new Promise((resolve) => setTimeout(resolve, 400));
return mockSequences.filter((s) => s.template_id === templateId);
},
testTemplate: async (templateId: number, data: any): Promise<TestGenerationResult> => {
await new Promise((resolve) => setTimeout(resolve, 500));
const template = mockTemplates.find(t => t.template_id === templateId);
if (!template) throw new Error("Template not found");
// Mock generation logic
let number = template.template_format;
number = number.replace("{ORG}", data.organization_id === "1" ? "PAT" : "ITD");
number = number.replace("{DOCTYPE}", template.document_type_id.toUpperCase());
number = number.replace("{DISC}", data.discipline_id === "1" ? "STR" : "ARC");
number = number.replace("{YYYY}", data.year.toString());
number = number.replace("{SEQ}", "0001");
return { number };
},
};

98
frontend/lib/api/rfas.ts Normal file
View File

@@ -0,0 +1,98 @@
import { RFA, CreateRFADto, RFAItem } from "@/types/rfa";
// Mock Data
const mockRFAs: RFA[] = [
{
rfa_id: 1,
rfa_number: "LCBP3-RFA-001",
subject: "Approval for Concrete Mix Design",
description: "Requesting approval for the proposed concrete mix design for foundations.",
contract_id: 1,
discipline_id: 1,
status: "PENDING",
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
contract_name: "Main Construction Contract",
discipline_name: "Civil",
items: [
{ id: 1, item_no: "1.1", description: "Concrete Mix Type A", quantity: 1, unit: "Lot", status: "PENDING" },
{ id: 2, item_no: "1.2", description: "Concrete Mix Type B", quantity: 1, unit: "Lot", status: "PENDING" },
],
},
{
rfa_id: 2,
rfa_number: "LCBP3-RFA-002",
subject: "Approval for Steel Reinforcement Shop Drawings",
description: "Shop drawings for Zone A foundations.",
contract_id: 1,
discipline_id: 2,
status: "APPROVED",
created_at: new Date(Date.now() - 172800000).toISOString(),
updated_at: new Date(Date.now() - 86400000).toISOString(),
contract_name: "Main Construction Contract",
discipline_name: "Structural",
items: [
{ id: 3, item_no: "1", description: "Shop Drawing Set A", quantity: 1, unit: "Set", status: "APPROVED" },
],
},
];
export const rfaApi = {
getAll: async (params?: { page?: number; status?: string; search?: string }) => {
await new Promise((resolve) => setTimeout(resolve, 500));
let filtered = [...mockRFAs];
if (params?.status) {
filtered = filtered.filter((r) => r.status === params.status);
}
if (params?.search) {
const lowerSearch = params.search.toLowerCase();
filtered = filtered.filter((r) =>
r.subject.toLowerCase().includes(lowerSearch) ||
r.rfa_number.toLowerCase().includes(lowerSearch)
);
}
return {
items: filtered,
total: filtered.length,
page: params?.page || 1,
totalPages: 1,
};
},
getById: async (id: number) => {
await new Promise((resolve) => setTimeout(resolve, 500));
return mockRFAs.find((r) => r.rfa_id === id);
},
create: async (data: CreateRFADto) => {
await new Promise((resolve) => setTimeout(resolve, 1000));
const newId = Math.max(...mockRFAs.map((r) => r.rfa_id)) + 1;
const newRFA: RFA = {
rfa_id: newId,
rfa_number: `LCBP3-RFA-00${newId}`,
...data,
status: "DRAFT",
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
contract_name: "Mock Contract",
discipline_name: "Mock Discipline",
items: data.items.map((item, index) => ({ ...item, id: index + 1, status: "PENDING" })),
};
mockRFAs.unshift(newRFA);
return newRFA;
},
updateStatus: async (id: number, status: RFA['status'], comments?: string) => {
await new Promise((resolve) => setTimeout(resolve, 800));
const rfa = mockRFAs.find((r) => r.rfa_id === id);
if (rfa) {
rfa.status = status;
rfa.updated_at = new Date().toISOString();
// In a real app, we'd log the comments and history
}
return rfa;
},
};

View File

@@ -0,0 +1,79 @@
import { SearchResult, SearchFilters } from "@/types/search";
// Mock Data
const mockResults: SearchResult[] = [
{
id: 1,
type: "correspondence",
title: "Submission of Monthly Report - Jan 2025",
description: "Please find attached the monthly progress report.",
status: "PENDING",
documentNumber: "LCBP3-COR-001",
createdAt: new Date().toISOString(),
highlight: "Submission of <b>Monthly Report</b> - Jan 2025",
},
{
id: 1,
type: "rfa",
title: "Approval for Concrete Mix Design",
description: "Requesting approval for the proposed concrete mix design.",
status: "PENDING",
documentNumber: "LCBP3-RFA-001",
createdAt: new Date().toISOString(),
highlight: "Approval for <b>Concrete Mix</b> Design",
},
{
id: 1,
type: "drawing",
title: "Ground Floor Plan",
description: "Architectural ground floor plan.",
status: "APPROVED",
documentNumber: "A-101",
createdAt: new Date(Date.now() - 100000000).toISOString(),
highlight: "Ground Floor <b>Plan</b>",
},
{
id: 2,
type: "correspondence",
title: "Request for Information regarding Foundation",
description: "Clarification needed on drawing A-101.",
status: "IN_REVIEW",
documentNumber: "LCBP3-COR-002",
createdAt: new Date(Date.now() - 86400000).toISOString(),
},
];
export const searchApi = {
search: async (filters: SearchFilters) => {
await new Promise((resolve) => setTimeout(resolve, 600));
let results = [...mockResults];
if (filters.query) {
const lowerQuery = filters.query.toLowerCase();
results = results.filter((r) =>
r.title.toLowerCase().includes(lowerQuery) ||
r.documentNumber.toLowerCase().includes(lowerQuery) ||
r.description?.toLowerCase().includes(lowerQuery)
);
}
if (filters.types && filters.types.length > 0) {
results = results.filter((r) => filters.types?.includes(r.type));
}
if (filters.statuses && filters.statuses.length > 0) {
results = results.filter((r) => filters.statuses?.includes(r.status));
}
return results;
},
suggest: async (query: string) => {
await new Promise((resolve) => setTimeout(resolve, 300));
const lowerQuery = query.toLowerCase();
return mockResults
.filter((r) => r.title.toLowerCase().includes(lowerQuery))
.slice(0, 5);
},
};

View File

@@ -0,0 +1,84 @@
import { Workflow, CreateWorkflowDto, ValidationResult } from "@/types/workflow";
// Mock Data
let mockWorkflows: Workflow[] = [
{
workflow_id: 1,
workflow_name: "Standard RFA Workflow",
description: "Default approval process for RFAs",
workflow_type: "RFA",
version: 1,
is_active: true,
dsl_definition: `name: Standard RFA Workflow
steps:
- name: Review
type: REVIEW
role: CM
next: Approval
- name: Approval
type: APPROVAL
role: PM`,
step_count: 2,
updated_at: new Date().toISOString(),
},
{
workflow_id: 2,
workflow_name: "Correspondence Review",
description: "Incoming correspondence review flow",
workflow_type: "CORRESPONDENCE",
version: 2,
is_active: true,
dsl_definition: `name: Correspondence Review
steps:
- name: Initial Review
type: REVIEW
role: DC`,
step_count: 1,
updated_at: new Date(Date.now() - 86400000).toISOString(),
},
];
export const workflowApi = {
getWorkflows: async (): Promise<Workflow[]> => {
await new Promise((resolve) => setTimeout(resolve, 500));
return [...mockWorkflows];
},
getWorkflow: async (id: number): Promise<Workflow | undefined> => {
await new Promise((resolve) => setTimeout(resolve, 300));
return mockWorkflows.find((w) => w.workflow_id === id);
},
createWorkflow: async (data: CreateWorkflowDto): Promise<Workflow> => {
await new Promise((resolve) => setTimeout(resolve, 800));
const newWorkflow: Workflow = {
workflow_id: Math.max(...mockWorkflows.map((w) => w.workflow_id)) + 1,
...data,
version: 1,
is_active: true,
step_count: 0, // Simplified for mock
updated_at: new Date().toISOString(),
};
mockWorkflows.push(newWorkflow);
return newWorkflow;
},
updateWorkflow: async (id: number, data: Partial<CreateWorkflowDto>): Promise<Workflow> => {
await new Promise((resolve) => setTimeout(resolve, 600));
const index = mockWorkflows.findIndex((w) => w.workflow_id === id);
if (index === -1) throw new Error("Workflow not found");
const updatedWorkflow = { ...mockWorkflows[index], ...data, updated_at: new Date().toISOString() };
mockWorkflows[index] = updatedWorkflow;
return updatedWorkflow;
},
validateDSL: async (dsl: string): Promise<ValidationResult> => {
await new Promise((resolve) => setTimeout(resolve, 400));
// Simple mock validation
if (!dsl.includes("name:") || !dsl.includes("steps:")) {
return { valid: false, errors: ["Missing 'name' or 'steps' field"] };
}
return { valid: true, errors: [] };
},
};