Compare commits

..

2 Commits

14 changed files with 274 additions and 222 deletions

View File

@ -1,2 +1,2 @@
import{a as s}from"./index-7JN9lLwu.js";import"./radix-vendor-DIkYAdWy.js";import"./charts-vendor-Bme4E5cb.js";import"./utils-vendor-DNMmNUQL.js";import"./ui-vendor-DbB0YGPu.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-B1UBYWWO.js";async function m(n){return(await s.post(`/conclusions/${n}/generate`)).data.data}async function f(n,t){return(await s.post(`/conclusions/${n}/finalize`,{finalRemark:t})).data.data}async function d(n){var t;try{return(await s.get(`/conclusions/${n}`)).data.data}catch(o){if(((t=o.response)==null?void 0:t.status)===404)return null;throw o}}export{f as finalizeConclusion,m as generateConclusion,d as getConclusion};
//# sourceMappingURL=conclusionApi-CMghC3Jo.js.map
import{a as s}from"./index-x1JLuWho.js";import"./radix-vendor-DIkYAdWy.js";import"./charts-vendor-Bme4E5cb.js";import"./utils-vendor-DNMmNUQL.js";import"./ui-vendor-DbB0YGPu.js";import"./socket-vendor-TjCxX7sJ.js";import"./redux-vendor-tbZCm13o.js";import"./router-vendor-B1UBYWWO.js";async function m(n){return(await s.post(`/conclusions/${n}/generate`)).data.data}async function f(n,t){return(await s.post(`/conclusions/${n}/finalize`,{finalRemark:t})).data.data}async function d(n){var t;try{return(await s.get(`/conclusions/${n}`)).data.data}catch(o){if(((t=o.response)==null?void 0:t.status)===404)return null;throw o}}export{f as finalizeConclusion,m as generateConclusion,d as getConclusion};
//# sourceMappingURL=conclusionApi-DX2Gwh6C.js.map

View File

@ -1 +1 @@
{"version":3,"file":"conclusionApi-CMghC3Jo.js","sources":["../../src/services/conclusionApi.ts"],"sourcesContent":["import apiClient from './authApi';\r\n\r\nexport interface ConclusionRemark {\r\n conclusionId: string;\r\n requestId: string;\r\n aiGeneratedRemark: string | null;\r\n aiModelUsed: string | null;\r\n aiConfidenceScore: number | null;\r\n finalRemark: string | null;\r\n editedBy: string | null;\r\n isEdited: boolean;\r\n editCount: number;\r\n approvalSummary: any;\r\n documentSummary: any;\r\n keyDiscussionPoints: string[];\r\n generatedAt: string | null;\r\n finalizedAt: string | null;\r\n createdAt: string;\r\n updatedAt: string;\r\n}\r\n\r\n/**\r\n * Generate AI-powered conclusion remark\r\n */\r\nexport async function generateConclusion(requestId: string): Promise<{\r\n conclusionId: string;\r\n aiGeneratedRemark: string;\r\n keyDiscussionPoints: string[];\r\n confidence: number;\r\n generatedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/generate`);\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Update conclusion remark (edit by initiator)\r\n */\r\nexport async function updateConclusion(requestId: string, finalRemark: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.put(`/conclusions/${requestId}`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Finalize conclusion and close request\r\n */\r\nexport async function finalizeConclusion(requestId: string, finalRemark: string): Promise<{\r\n conclusionId: string;\r\n requestNumber: string;\r\n status: string;\r\n finalRemark: string;\r\n finalizedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/finalize`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Get conclusion for a request\r\n * Returns null if conclusion doesn't exist (404) instead of throwing error\r\n */\r\nexport async function getConclusion(requestId: string): Promise<ConclusionRemark | null> {\r\n try {\r\n const response = await apiClient.get(`/conclusions/${requestId}`);\r\n return response.data.data;\r\n } catch (error: any) {\r\n // Handle 404 gracefully - conclusion doesn't exist yet, which is normal\r\n if (error.response?.status === 404) {\r\n return null;\r\n }\r\n // Re-throw other errors\r\n throw error;\r\n }\r\n}\r\n\r\n"],"names":["generateConclusion","requestId","apiClient","finalizeConclusion","finalRemark","getConclusion","error","_a"],"mappings":"6RAwBA,eAAsBA,EAAmBC,EAMtC,CAED,OADiB,MAAMC,EAAU,KAAK,gBAAgBD,CAAS,WAAW,GAC1D,KAAK,IACvB,CAaA,eAAsBE,EAAmBF,EAAmBG,EAMzD,CAED,OADiB,MAAMF,EAAU,KAAK,gBAAgBD,CAAS,YAAa,CAAE,YAAAG,EAAa,GAC3E,KAAK,IACvB,CAMA,eAAsBC,EAAcJ,EAAqD,OACvF,GAAI,CAEF,OADiB,MAAMC,EAAU,IAAI,gBAAgBD,CAAS,EAAE,GAChD,KAAK,IACvB,OAASK,EAAY,CAEnB,KAAIC,EAAAD,EAAM,WAAN,YAAAC,EAAgB,UAAW,IAC7B,OAAO,KAGT,MAAMD,CACR,CACF"}
{"version":3,"file":"conclusionApi-DX2Gwh6C.js","sources":["../../src/services/conclusionApi.ts"],"sourcesContent":["import apiClient from './authApi';\r\n\r\nexport interface ConclusionRemark {\r\n conclusionId: string;\r\n requestId: string;\r\n aiGeneratedRemark: string | null;\r\n aiModelUsed: string | null;\r\n aiConfidenceScore: number | null;\r\n finalRemark: string | null;\r\n editedBy: string | null;\r\n isEdited: boolean;\r\n editCount: number;\r\n approvalSummary: any;\r\n documentSummary: any;\r\n keyDiscussionPoints: string[];\r\n generatedAt: string | null;\r\n finalizedAt: string | null;\r\n createdAt: string;\r\n updatedAt: string;\r\n}\r\n\r\n/**\r\n * Generate AI-powered conclusion remark\r\n */\r\nexport async function generateConclusion(requestId: string): Promise<{\r\n conclusionId: string;\r\n aiGeneratedRemark: string;\r\n keyDiscussionPoints: string[];\r\n confidence: number;\r\n generatedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/generate`);\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Update conclusion remark (edit by initiator)\r\n */\r\nexport async function updateConclusion(requestId: string, finalRemark: string): Promise<ConclusionRemark> {\r\n const response = await apiClient.put(`/conclusions/${requestId}`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Finalize conclusion and close request\r\n */\r\nexport async function finalizeConclusion(requestId: string, finalRemark: string): Promise<{\r\n conclusionId: string;\r\n requestNumber: string;\r\n status: string;\r\n finalRemark: string;\r\n finalizedAt: string;\r\n}> {\r\n const response = await apiClient.post(`/conclusions/${requestId}/finalize`, { finalRemark });\r\n return response.data.data;\r\n}\r\n\r\n/**\r\n * Get conclusion for a request\r\n * Returns null if conclusion doesn't exist (404) instead of throwing error\r\n */\r\nexport async function getConclusion(requestId: string): Promise<ConclusionRemark | null> {\r\n try {\r\n const response = await apiClient.get(`/conclusions/${requestId}`);\r\n return response.data.data;\r\n } catch (error: any) {\r\n // Handle 404 gracefully - conclusion doesn't exist yet, which is normal\r\n if (error.response?.status === 404) {\r\n return null;\r\n }\r\n // Re-throw other errors\r\n throw error;\r\n }\r\n}\r\n\r\n"],"names":["generateConclusion","requestId","apiClient","finalizeConclusion","finalRemark","getConclusion","error","_a"],"mappings":"6RAwBA,eAAsBA,EAAmBC,EAMtC,CAED,OADiB,MAAMC,EAAU,KAAK,gBAAgBD,CAAS,WAAW,GAC1D,KAAK,IACvB,CAaA,eAAsBE,EAAmBF,EAAmBG,EAMzD,CAED,OADiB,MAAMF,EAAU,KAAK,gBAAgBD,CAAS,YAAa,CAAE,YAAAG,EAAa,GAC3E,KAAK,IACvB,CAMA,eAAsBC,EAAcJ,EAAqD,OACvF,GAAI,CAEF,OADiB,MAAMC,EAAU,IAAI,gBAAgBD,CAAS,EAAE,GAChD,KAAK,IACvB,OAASK,EAAY,CAEnB,KAAIC,EAAAD,EAAM,WAAN,YAAAC,EAAgB,UAAW,IAC7B,OAAO,KAGT,MAAMD,CACR,CACF"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -52,7 +52,7 @@
transition: transform 0.2s ease;
}
</style>
<script type="module" crossorigin src="/assets/index-7JN9lLwu.js"></script>
<script type="module" crossorigin src="/assets/index-x1JLuWho.js"></script>
<link rel="modulepreload" crossorigin href="/assets/charts-vendor-Bme4E5cb.js">
<link rel="modulepreload" crossorigin href="/assets/radix-vendor-DIkYAdWy.js">
<link rel="modulepreload" crossorigin href="/assets/utils-vendor-DNMmNUQL.js">
@ -60,7 +60,7 @@
<link rel="modulepreload" crossorigin href="/assets/socket-vendor-TjCxX7sJ.js">
<link rel="modulepreload" crossorigin href="/assets/redux-vendor-tbZCm13o.js">
<link rel="modulepreload" crossorigin href="/assets/router-vendor-B1UBYWWO.js">
<link rel="stylesheet" crossorigin href="/assets/index-B-mLDzJe.css">
<link rel="stylesheet" crossorigin href="/assets/index-CNlPctO6.css">
</head>
<body>
<div id="root"></div>

View File

@ -3,6 +3,16 @@ import dotenv from 'dotenv';
dotenv.config();
// 1. Debugging: Print what the app actually sees
console.log('--- Database Config Debug ---');
console.log(`DB_HOST: ${process.env.DB_HOST}`);
console.log(`DB_SSL (Raw): '${process.env.DB_SSL}`); // Quotes help see trailing spaces
// 2. Fix: Trim whitespace to ensure "true " becomes "true"
const isSSL = (process.env.DB_SSL || '').trim() === 'true';
console.log(`SSL Enabled: ${isSSL}`);
console.log('---------------------------');
const sequelize = new Sequelize({
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432', 10),
@ -10,7 +20,7 @@ const sequelize = new Sequelize({
username: process.env.DB_USER || 'postgres',
password: process.env.DB_PASSWORD || 'postgres',
dialect: 'postgres',
logging: false, // Disable SQL query logging for cleaner console output
logging: false,
pool: {
min: parseInt(process.env.DB_POOL_MIN || '2', 10),
max: parseInt(process.env.DB_POOL_MAX || '10', 10),
@ -18,7 +28,8 @@ const sequelize = new Sequelize({
idle: 10000,
},
dialectOptions: {
ssl: process.env.DB_SSL === 'true' ? {
// 3. Use the robust boolean we calculated above
ssl: isSSL ? {
require: true,
rejectUnauthorized: false,
} : false,

View File

@ -236,6 +236,7 @@ export class WorkflowController {
priority: validated.priority as Priority,
approvalLevels: enrichedApprovalLevels,
participants: autoGeneratedParticipants,
isDraft: parsed.isDraft === true, // Submit by default unless isDraft is explicitly true
} as any;
const requestMeta = getRequestMetadata(req);
@ -682,7 +683,10 @@ export class WorkflowController {
}
const parsed = JSON.parse(raw);
const validated = validateUpdateWorkflow(parsed);
const updateData: UpdateWorkflowRequest = { ...validated } as any;
const updateData: UpdateWorkflowRequest = {
...validated,
isDraft: parsed.isDraft !== undefined ? (parsed.isDraft === true) : undefined
} as any;
if (validated.priority) {
updateData.priority = validated.priority === 'EXPRESS' ? Priority.EXPRESS : Priority.STANDARD;
}

View File

@ -22,7 +22,7 @@ dotenv.config({ path: path.resolve(__dirname, '../../.env') });
const execAsync = promisify(exec);
// DB constants moved inside functions to ensure secrets are loaded first
const isSSL = (process.env.DB_SSL || '').trim() === 'true';
async function checkAndCreateDatabase(): Promise<boolean> {
const DB_HOST = process.env.DB_HOST || 'localhost';
const DB_PORT = parseInt(process.env.DB_PORT || '5432');
@ -36,6 +36,7 @@ async function checkAndCreateDatabase(): Promise<boolean> {
user: DB_USER,
password: DB_PASSWORD,
database: 'postgres', // Connect to default postgres database
ssl: isSSL ? { rejectUnauthorized: false } : undefined,
});
try {
@ -64,6 +65,7 @@ async function checkAndCreateDatabase(): Promise<boolean> {
user: DB_USER,
password: DB_PASSWORD,
database: DB_NAME,
ssl: isSSL ? { rejectUnauthorized: false } : undefined,
});
await newDbClient.connect();

View File

@ -337,7 +337,7 @@ class GoogleSecretManagerService {
private getDefaultSecretNames(): string[] {
return [
// Database
//'DB_PASSWORD',
'DB_PASSWORD',
// JWT & Session
'JWT_SECRET',

View File

@ -2450,6 +2450,9 @@ export class WorkflowService {
try {
const requestNumber = await generateRequestNumber();
const totalTatHours = workflowData.approvalLevels.reduce((sum, level) => sum + level.tatHours, 0);
const isDraftRequested = workflowData.isDraft === true;
const initialStatus = isDraftRequested ? WorkflowStatus.DRAFT : WorkflowStatus.PENDING;
const now = new Date();
const workflow = await WorkflowRequest.create({
requestNumber,
@ -2461,9 +2464,10 @@ export class WorkflowService {
currentLevel: 1,
totalLevels: workflowData.approvalLevels.length,
totalTatHours,
status: WorkflowStatus.DRAFT,
isDraft: true,
isDeleted: false
status: initialStatus,
isDraft: isDraftRequested,
isDeleted: false,
submissionDate: isDraftRequested ? undefined : now
});
// Create approval levels
@ -2549,15 +2553,18 @@ export class WorkflowService {
type: 'created',
user: { userId: initiatorId, name: initiatorName },
timestamp: new Date().toISOString(),
action: 'Initial request submitted',
details: `Initial request submitted for ${workflowData.title} by ${initiatorName}`,
action: isDraftRequested ? 'Draft request created' : 'Initial request submitted',
details: isDraftRequested
? `Draft request "${workflowData.title}" created by ${initiatorName}`
: `Initial request submitted for ${workflowData.title} by ${initiatorName}`,
ipAddress: requestMetadata?.ipAddress || undefined,
userAgent: requestMetadata?.userAgent || undefined
});
// NOTE: Notifications are NOT sent here because workflows are created as DRAFTS
// Notifications will be sent in submitWorkflow() when the draft is actually submitted
// This prevents approvers from being notified about draft requests
// If not a draft, initiate the workflow (approvals, notifications, etc.)
if (!isDraftRequested) {
return await this.internalSubmitWorkflow(workflow, now);
}
return workflow;
} catch (error) {
@ -3112,6 +3119,9 @@ export class WorkflowService {
// Only allow full updates (approval levels, participants) for DRAFT workflows
const isDraft = (workflow as any).status === WorkflowStatus.DRAFT || (workflow as any).isDraft;
// Determine if this is a transition from draft to submitted
const isTransitioningToSubmitted = updateData.isDraft === false && (workflow as any).isDraft;
// Update basic workflow fields
const basicUpdate: any = {};
if (updateData.title) basicUpdate.title = updateData.title;
@ -3119,6 +3129,13 @@ export class WorkflowService {
if (updateData.priority) basicUpdate.priority = updateData.priority;
if (updateData.status) basicUpdate.status = updateData.status;
if (updateData.conclusionRemark !== undefined) basicUpdate.conclusionRemark = updateData.conclusionRemark;
if (updateData.isDraft !== undefined) basicUpdate.isDraft = updateData.isDraft;
// If transitioning, ensure status and submissionDate are set
if (isTransitioningToSubmitted) {
basicUpdate.status = WorkflowStatus.PENDING;
basicUpdate.submissionDate = new Date();
}
await workflow.update(basicUpdate);
@ -3267,8 +3284,13 @@ export class WorkflowService {
logger.info(`Marked ${deleteResult[0]} documents as deleted in database (out of ${updateData.deleteDocumentIds.length} requested)`);
}
// Reload the workflow instance to get latest data (without associations to avoid the error)
// The associations issue occurs when trying to include them, so we skip that
// If transitioning, call the internal submission logic (notifications, TAT, etc.)
if (isTransitioningToSubmitted) {
logger.info(`[Workflow] Transitioning draft ${actualRequestId} to submitted state`);
return await this.internalSubmitWorkflow(workflow, (workflow as any).submissionDate);
}
// Reload the workflow instance to get latest data
const refreshed = await WorkflowRequest.findByPk(actualRequestId);
return refreshed;
} catch (error) {
@ -3290,12 +3312,24 @@ export class WorkflowService {
const workflow = await this.findWorkflowByIdentifier(requestId);
if (!workflow) return null;
const now = new Date();
return await this.internalSubmitWorkflow(workflow, now);
} catch (error) {
logger.error(`Failed to submit workflow ${requestId}:`, error);
throw new Error('Failed to submit workflow');
}
}
/**
* Internal method to handle workflow submission logic (status update, notifications, TAT scheduling)
* Centralized here to support both direct creation-submission and draft-to-submission flows.
*/
private async internalSubmitWorkflow(workflow: WorkflowRequest, now: Date): Promise<WorkflowRequest> {
// Get the actual requestId (UUID) - handle both UUID and requestNumber cases
const actualRequestId = (workflow as any).getDataValue
? (workflow as any).getDataValue('requestId')
: (workflow as any).requestId;
const now = new Date();
const updated = await workflow.update({
status: WorkflowStatus.PENDING,
isDraft: false,
@ -3440,10 +3474,7 @@ export class WorkflowService {
logger.error(`[Workflow] Failed to send spectator notifications for request ${requestNumber} (requestId: ${actualRequestId}):`, spectatorError);
// Don't fail the submission if spectator notifications fail
}
return updated;
} catch (error) {
logger.error(`Failed to submit workflow ${requestId}:`, error);
throw new Error('Failed to submit workflow');
}
}
}

View File

@ -29,6 +29,7 @@ export interface CreateWorkflowRequest {
priority: Priority;
approvalLevels: CreateApprovalLevel[];
participants?: CreateParticipant[];
isDraft?: boolean;
}
export interface UpdateWorkflowRequest {
@ -42,6 +43,7 @@ export interface UpdateWorkflowRequest {
participants?: CreateParticipant[];
// Document updates (add new documents via multipart, delete via IDs)
deleteDocumentIds?: string[];
isDraft?: boolean;
}
export interface CreateApprovalLevel {

View File

@ -44,6 +44,7 @@ export const createWorkflowSchema = z.object({
priorityUi: z.string().optional(),
templateId: z.string().optional(),
ccList: z.array(z.any()).optional(),
isDraft: z.boolean().optional(),
});
export const updateWorkflowSchema = z.object({
@ -73,6 +74,7 @@ export const updateWorkflowSchema = z.object({
notificationEnabled: z.boolean().optional(),
})).optional(),
deleteDocumentIds: z.array(z.string().uuid()).optional(),
isDraft: z.boolean().optional(),
});
// Helper to validate UUID or requestNumber format