From fb81306a6868e68f77363967d0f9acaf24f27bd7 Mon Sep 17 00:00:00 2001 From: firdausfazil Date: Mon, 16 Jun 2025 00:52:49 +0800 Subject: [PATCH] process initial --- error.vue | 113 +++--- pages/execution/form/[id].vue | 360 ++++++++++++++++++ pages/execution/new-case.vue | 193 +++++++--- plugins/route-guard.js | 28 ++ prisma/json/json-schema.json | 236 ++++++++++++ .../20230808013656_initialize/migration.sql | 72 ---- .../migration.sql | 56 +++ .../migration.toml | 4 + prisma/schema.prisma | 66 ++++ server/api/cases/[id]/forms.get.js | 184 +++++++++ server/api/process/[id]/start.post.js | 174 +++++++++ server/api/process/pending.get.js | 41 ++ server/api/tasks/[id].get.js | 122 ++++++ server/api/tasks/[id]/save-draft.post.js | 72 ++++ server/api/tasks/[id]/submit.post.js | 122 ++++++ server/api/tasks/index.get.js | 96 +++++ server/middleware/dynamic-routes.js | 9 + 17 files changed, 1770 insertions(+), 178 deletions(-) create mode 100644 pages/execution/form/[id].vue create mode 100644 plugins/route-guard.js delete mode 100644 prisma/migrations/20230808013656_initialize/migration.sql create mode 100644 prisma/migrations/20240321000001_add_case_instance_and_task_tables_v2/migration.sql create mode 100644 prisma/migrations/20240321000001_add_case_instance_and_task_tables_v2/migration.toml create mode 100644 server/api/cases/[id]/forms.get.js create mode 100644 server/api/process/[id]/start.post.js create mode 100644 server/api/process/pending.get.js create mode 100644 server/api/tasks/[id].get.js create mode 100644 server/api/tasks/[id]/save-draft.post.js create mode 100644 server/api/tasks/[id]/submit.post.js create mode 100644 server/api/tasks/index.get.js create mode 100644 server/middleware/dynamic-routes.js diff --git a/error.vue b/error.vue index 55540fe..e34a2db 100644 --- a/error.vue +++ b/error.vue @@ -1,68 +1,85 @@ diff --git a/plugins/route-guard.js b/plugins/route-guard.js new file mode 100644 index 0000000..e4095f2 --- /dev/null +++ b/plugins/route-guard.js @@ -0,0 +1,28 @@ +export default defineNuxtPlugin((nuxtApp) => { + nuxtApp.hook('app:created', () => { + // Check if we're on the client side + if (process.client) { + const router = useRouter(); + + // Add global navigation guard + router.beforeEach((to, from, next) => { + // Check if the route path contains literal square brackets + // This indicates a user is trying to navigate to a route with [param] directly + if (to.fullPath.includes('/[') || to.fullPath.includes(']')) { + console.warn('Invalid route detected with literal brackets:', to.fullPath); + + // Extract the route pattern without the brackets + const baseRoute = to.fullPath.split('/').slice(0, -1).join('/'); + + // Redirect to a more appropriate page + return next({ + path: baseRoute || '/', + query: { error: 'invalid_route_format' } + }); + } + + next(); + }); + } + }); +}); \ No newline at end of file diff --git a/prisma/json/json-schema.json b/prisma/json/json-schema.json index 492b4a9..321d8ec 100644 --- a/prisma/json/json-schema.json +++ b/prisma/json/json-schema.json @@ -92,6 +92,24 @@ "items": { "$ref": "#/definitions/userrole" } + }, + "startedCases": { + "type": "array", + "items": { + "$ref": "#/definitions/caseInstance" + } + }, + "assignedTasks": { + "type": "array", + "items": { + "$ref": "#/definitions/task" + } + }, + "caseTimelineEntries": { + "type": "array", + "items": { + "$ref": "#/definitions/caseTimeline" + } } } }, @@ -246,6 +264,12 @@ "items": { "$ref": "#/definitions/formHistory" } + }, + "tasks": { + "type": "array", + "items": { + "$ref": "#/definitions/task" + } } } }, @@ -464,6 +488,12 @@ "items": { "$ref": "#/definitions/processHistory" } + }, + "cases": { + "type": "array", + "items": { + "$ref": "#/definitions/caseInstance" + } } } }, @@ -582,6 +612,203 @@ ] } } + }, + "caseInstance": { + "type": "object", + "properties": { + "caseID": { + "type": "integer" + }, + "caseUUID": { + "type": "string" + }, + "caseName": { + "type": "string" + }, + "caseStatus": { + "type": "string", + "default": "active" + }, + "caseVariables": { + "type": [ + "number", + "string", + "boolean", + "object", + "array", + "null" + ] + }, + "caseSettings": { + "type": [ + "number", + "string", + "boolean", + "object", + "array", + "null" + ] + }, + "caseDefinition": { + "type": [ + "number", + "string", + "boolean", + "object", + "array", + "null" + ] + }, + "caseCreatedDate": { + "type": "string", + "format": "date-time" + }, + "caseModifiedDate": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "caseCompletedDate": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "process": { + "$ref": "#/definitions/process" + }, + "startedBy": { + "anyOf": [ + { + "$ref": "#/definitions/user" + }, + { + "type": "null" + } + ] + }, + "tasks": { + "type": "array", + "items": { + "$ref": "#/definitions/task" + } + }, + "timeline": { + "type": "array", + "items": { + "$ref": "#/definitions/caseTimeline" + } + } + } + }, + "task": { + "type": "object", + "properties": { + "taskID": { + "type": "integer" + }, + "taskUUID": { + "type": "string" + }, + "taskName": { + "type": "string" + }, + "taskType": { + "type": "string" + }, + "taskStatus": { + "type": "string", + "default": "pending" + }, + "taskData": { + "type": [ + "number", + "string", + "boolean", + "object", + "array", + "null" + ] + }, + "taskCreatedDate": { + "type": "string", + "format": "date-time" + }, + "taskModifiedDate": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "taskCompletedDate": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "case": { + "$ref": "#/definitions/caseInstance" + }, + "assignedTo": { + "anyOf": [ + { + "$ref": "#/definitions/user" + }, + { + "type": "null" + } + ] + }, + "form": { + "anyOf": [ + { + "$ref": "#/definitions/form" + }, + { + "type": "null" + } + ] + } + } + }, + "caseTimeline": { + "type": "object", + "properties": { + "timelineID": { + "type": "integer" + }, + "timelineType": { + "type": "string" + }, + "timelineDescription": { + "type": [ + "string", + "null" + ] + }, + "timelineDate": { + "type": "string", + "format": "date-time" + }, + "case": { + "$ref": "#/definitions/caseInstance" + }, + "createdBy": { + "anyOf": [ + { + "$ref": "#/definitions/user" + }, + { + "type": "null" + } + ] + } + } } }, "type": "object", @@ -606,6 +833,15 @@ }, "processHistory": { "$ref": "#/definitions/processHistory" + }, + "caseInstance": { + "$ref": "#/definitions/caseInstance" + }, + "task": { + "$ref": "#/definitions/task" + }, + "caseTimeline": { + "$ref": "#/definitions/caseTimeline" } } } \ No newline at end of file diff --git a/prisma/migrations/20230808013656_initialize/migration.sql b/prisma/migrations/20230808013656_initialize/migration.sql deleted file mode 100644 index c611a65..0000000 --- a/prisma/migrations/20230808013656_initialize/migration.sql +++ /dev/null @@ -1,72 +0,0 @@ --- CreateTable -CREATE TABLE `audit` ( - `auditID` INTEGER NOT NULL AUTO_INCREMENT, - `auditIP` VARCHAR(255) NULL, - `auditURL` VARCHAR(255) NULL, - `auditURLMethod` VARCHAR(255) NULL, - `auditURLPayload` VARCHAR(255) NULL, - `auditCreatedDate` DATETIME(0) NULL, - - PRIMARY KEY (`auditID`) -) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - --- CreateTable -CREATE TABLE `user` ( - `userID` INTEGER NOT NULL AUTO_INCREMENT, - `userSecretKey` VARCHAR(255) NULL, - `userUsername` VARCHAR(255) NULL, - `userPassword` VARCHAR(255) NULL, - `userFullName` VARCHAR(255) NULL, - `userEmail` VARCHAR(255) NULL, - `userPhone` VARCHAR(255) NULL, - `userStatus` VARCHAR(255) NULL, - `userCreatedDate` DATETIME(0) NULL, - `userModifiedDate` DATETIME(0) NULL, - - PRIMARY KEY (`userID`) -) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - --- CreateTable -CREATE TABLE `role` ( - `roleID` INTEGER NOT NULL AUTO_INCREMENT, - `roleName` VARCHAR(255) NULL, - `roleDescription` VARCHAR(255) NULL, - `roleStatus` VARCHAR(255) NULL, - `roleCreatedDate` DATETIME(0) NULL, - `roleModifiedDate` DATETIME(0) NULL, - - PRIMARY KEY (`roleID`) -) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - --- CreateTable -CREATE TABLE `lookup` ( - `lookupID` INTEGER NOT NULL AUTO_INCREMENT, - `lookupOrder` INTEGER NULL, - `lookupTitle` VARCHAR(255) NULL, - `lookupRefCode` VARCHAR(255) NULL, - `lookupValue` VARCHAR(255) NULL, - `lookupType` VARCHAR(255) NULL, - `lookupStatus` VARCHAR(255) NULL, - `lookupCreatedDate` DATETIME(0) NULL, - `lookupModifiedDate` DATETIME(0) NULL, - - PRIMARY KEY (`lookupID`) -) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - --- CreateTable -CREATE TABLE `userrole` ( - `userRoleID` INTEGER NOT NULL AUTO_INCREMENT, - `userRoleUserID` INTEGER NOT NULL DEFAULT 0, - `userRoleRoleID` INTEGER NOT NULL DEFAULT 0, - `userRoleCreatedDate` DATETIME(0) NOT NULL, - - INDEX `FK_userrole_role`(`userRoleRoleID`), - INDEX `FK_userrole_user`(`userRoleUserID`), - PRIMARY KEY (`userRoleID`) -) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - --- AddForeignKey -ALTER TABLE `userrole` ADD CONSTRAINT `FK_userrole_role` FOREIGN KEY (`userRoleRoleID`) REFERENCES `role`(`roleID`) ON DELETE NO ACTION ON UPDATE NO ACTION; - --- AddForeignKey -ALTER TABLE `userrole` ADD CONSTRAINT `FK_userrole_user` FOREIGN KEY (`userRoleUserID`) REFERENCES `user`(`userID`) ON DELETE NO ACTION ON UPDATE NO ACTION; diff --git a/prisma/migrations/20240321000001_add_case_instance_and_task_tables_v2/migration.sql b/prisma/migrations/20240321000001_add_case_instance_and_task_tables_v2/migration.sql new file mode 100644 index 0000000..9da3ad9 --- /dev/null +++ b/prisma/migrations/20240321000001_add_case_instance_and_task_tables_v2/migration.sql @@ -0,0 +1,56 @@ +-- CreateTable +CREATE TABLE `caseInstance` ( + `caseID` INTEGER NOT NULL AUTO_INCREMENT, + `caseUUID` VARCHAR(36) NOT NULL, + `processID` INTEGER NOT NULL, + `caseName` VARCHAR(255) NOT NULL, + `caseStatus` VARCHAR(50) NOT NULL DEFAULT 'active', + `caseStartedBy` INTEGER NULL, + `caseVariables` JSON NULL, + `caseSettings` JSON NULL, + `caseDefinition` JSON NULL, + `caseCreatedDate` DATETIME(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0), + `caseModifiedDate` DATETIME(0) NULL, + `caseCompletedDate` DATETIME(0) NULL, + + UNIQUE INDEX `caseInstance_caseUUID_key`(`caseUUID`), + INDEX `FK_case_process`(`processID`), + INDEX `FK_case_startedBy`(`caseStartedBy`), + INDEX `IDX_case_status`(`caseStatus`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `task` ( + `taskID` INTEGER NOT NULL AUTO_INCREMENT, + `taskUUID` VARCHAR(36) NOT NULL, + `caseID` INTEGER NOT NULL, + `taskName` VARCHAR(255) NOT NULL, + `taskType` VARCHAR(50) NOT NULL, + `taskStatus` VARCHAR(50) NOT NULL DEFAULT 'pending', + `taskAssignedTo` INTEGER NULL, + `taskFormID` INTEGER NULL, + `taskData` JSON NULL, + `taskCreatedDate` DATETIME(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0), + `taskModifiedDate` DATETIME(0) NULL, + `taskCompletedDate` DATETIME(0) NULL, + + UNIQUE INDEX `task_taskUUID_key`(`taskUUID`), + INDEX `FK_task_case`(`caseID`), + INDEX `FK_task_assignedTo`(`taskAssignedTo`), + INDEX `FK_task_form`(`taskFormID`), + INDEX `IDX_task_status`(`taskStatus`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +-- CreateTable +CREATE TABLE `caseTimeline` ( + `timelineID` INTEGER NOT NULL AUTO_INCREMENT, + `caseID` INTEGER NOT NULL, + `timelineType` VARCHAR(50) NOT NULL, + `timelineDescription` TEXT NULL, + `timelineDate` DATETIME(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0), + `timelineCreatedBy` INTEGER NULL, + + INDEX `FK_caseTimeline_case`(`caseID`), + INDEX `FK_caseTimeline_createdBy`(`timelineCreatedBy`), + INDEX `IDX_caseTimeline_date`(`timelineDate`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; \ No newline at end of file diff --git a/prisma/migrations/20240321000001_add_case_instance_and_task_tables_v2/migration.toml b/prisma/migrations/20240321000001_add_case_instance_and_task_tables_v2/migration.toml new file mode 100644 index 0000000..008dd21 --- /dev/null +++ b/prisma/migrations/20240321000001_add_case_instance_and_task_tables_v2/migration.toml @@ -0,0 +1,4 @@ +# This is an empty migration. + +migration_name = "20240321000001_add_case_instance_and_task_tables_v2" +migration_hash = "20240321000001_add_case_instance_and_task_tables_v2" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 3da984a..d26580d 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -28,6 +28,9 @@ model user { processes process[] @relation("ProcessCreator") processHistoryEntries processHistory[] userrole userrole[] + startedCases caseInstance[] @relation("CaseStartedBy") + assignedTasks task[] @relation("TaskAssignedTo") + caseTimelineEntries caseTimeline[] } model role { @@ -68,6 +71,7 @@ model form { scriptMode String? @default("safe") @db.VarChar(20) creator user? @relation("FormCreator", fields: [formCreatedBy], references: [userID]) history formHistory[] @relation("FormHistoryEntries") + tasks task[] @@index([formCreatedBy], map: "FK_form_creator") } @@ -119,6 +123,7 @@ model process { processDeletedDate DateTime? @db.DateTime(0) creator user? @relation("ProcessCreator", fields: [processCreatedBy], references: [userID]) history processHistory[] @relation("ProcessHistoryEntries") + cases caseInstance[] @@index([processCreatedBy], map: "FK_process_creator") @@index([processStatus], map: "IDX_process_status") @@ -154,3 +159,64 @@ model processHistory { @@index([processUUID], map: "IDX_processHistory_uuid") @@index([savedDate], map: "IDX_processHistory_date") } + +model caseInstance { + caseID Int @id @default(autoincrement()) + caseUUID String @unique @db.VarChar(36) + processID Int + caseName String @db.VarChar(255) + caseStatus String @default("active") @db.VarChar(50) + caseStartedBy Int? + caseVariables Json? + caseSettings Json? + caseDefinition Json? + caseCreatedDate DateTime @default(now()) @db.DateTime(0) + caseModifiedDate DateTime? @updatedAt @db.DateTime(0) + caseCompletedDate DateTime? @db.DateTime(0) + process process @relation(fields: [processID], references: [processID]) + startedBy user? @relation("CaseStartedBy", fields: [caseStartedBy], references: [userID]) + tasks task[] + timeline caseTimeline[] + + @@index([processID], map: "FK_case_process") + @@index([caseStartedBy], map: "FK_case_startedBy") + @@index([caseStatus], map: "IDX_case_status") +} + +model task { + taskID Int @id @default(autoincrement()) + taskUUID String @unique @db.VarChar(36) + caseID Int + taskName String @db.VarChar(255) + taskType String @db.VarChar(50) + taskStatus String @default("pending") @db.VarChar(50) + taskAssignedTo Int? + taskFormID Int? + taskData Json? + taskCreatedDate DateTime @default(now()) @db.DateTime(0) + taskModifiedDate DateTime? @updatedAt @db.DateTime(0) + taskCompletedDate DateTime? @db.DateTime(0) + case caseInstance @relation(fields: [caseID], references: [caseID]) + assignedTo user? @relation("TaskAssignedTo", fields: [taskAssignedTo], references: [userID]) + form form? @relation(fields: [taskFormID], references: [formID]) + + @@index([caseID], map: "FK_task_case") + @@index([taskAssignedTo], map: "FK_task_assignedTo") + @@index([taskFormID], map: "FK_task_form") + @@index([taskStatus], map: "IDX_task_status") +} + +model caseTimeline { + timelineID Int @id @default(autoincrement()) + caseID Int + timelineType String @db.VarChar(50) + timelineDescription String? @db.Text + timelineDate DateTime @default(now()) @db.DateTime(0) + timelineCreatedBy Int? + case caseInstance @relation(fields: [caseID], references: [caseID]) + createdBy user? @relation(fields: [timelineCreatedBy], references: [userID]) + + @@index([caseID], map: "FK_caseTimeline_case") + @@index([timelineCreatedBy], map: "FK_caseTimeline_createdBy") + @@index([timelineDate], map: "IDX_caseTimeline_date") +} diff --git a/server/api/cases/[id]/forms.get.js b/server/api/cases/[id]/forms.get.js new file mode 100644 index 0000000..7f0de23 --- /dev/null +++ b/server/api/cases/[id]/forms.get.js @@ -0,0 +1,184 @@ +import { PrismaClient } from '@prisma/client'; + +// Initialize Prisma client +const prisma = new PrismaClient(); + +export default defineEventHandler(async (event) => { + try { + // Get the case ID from the route parameter + const caseId = getRouterParam(event, 'id'); + + if (!caseId) { + return { + success: false, + error: 'Case ID is required' + }; + } + + // Check if the ID is a UUID or numeric ID + const isUUID = caseId.length === 36 && caseId.includes('-'); + + // Find the case instance by UUID or ID + const caseInstance = await prisma.caseInstance.findFirst({ + where: isUUID + ? { caseUUID: caseId } + : { caseID: parseInt(caseId) }, + include: { + process: { + select: { + processID: true, + processUUID: true, + processName: true, + processDescription: true + } + }, + startedBy: { + select: { + userID: true, + userFullName: true, + userUsername: true + } + }, + tasks: { + include: { + form: { + select: { + formID: true, + formUUID: true, + formName: true, + formDescription: true, + formComponents: true, + formStatus: true, + customCSS: true, + customScript: true, + formEvents: true, + scriptMode: true + } + }, + assignedTo: { + select: { + userID: true, + userFullName: true, + userUsername: true + } + } + } + }, + timeline: { + orderBy: { + timelineDate: 'desc' + }, + include: { + createdBy: { + select: { + userID: true, + userFullName: true, + userUsername: true + } + } + } + } + } + }); + + if (!caseInstance) { + return { + success: false, + error: 'Case instance not found' + }; + } + + // Extract forms from tasks and remove duplicates + const forms = []; + const formIds = new Set(); + + for (const task of caseInstance.tasks) { + if (task.form) { + // Make sure formComponents is properly structured + let formComponents = []; + try { + if (task.form.formComponents) { + // Check if formComponents is already an array or needs to be extracted from a structure + if (Array.isArray(task.form.formComponents)) { + formComponents = task.form.formComponents; + } else if (task.form.formComponents.components && Array.isArray(task.form.formComponents.components)) { + formComponents = task.form.formComponents.components; + } else { + // Try to parse if it's a stringified JSON + const parsedComponents = typeof task.form.formComponents === 'string' + ? JSON.parse(task.form.formComponents) + : task.form.formComponents; + + if (Array.isArray(parsedComponents)) { + formComponents = parsedComponents; + } else if (parsedComponents.components && Array.isArray(parsedComponents.components)) { + formComponents = parsedComponents.components; + } + } + } + } catch (err) { + console.error('Error parsing form components:', err); + } + + // Extract form data from taskData if it exists + let formData = null; + if (task.taskData && task.taskData.formData) { + formData = task.taskData.formData; + } + + forms.push({ + ...task.form, + formComponents: formComponents, + taskId: task.taskID, + taskUUID: task.taskUUID, + taskStatus: task.taskStatus, + taskName: task.taskName, + taskData: task.taskData, + formData: formData, + submittedAt: task.taskData?.submittedAt || null, + completedDate: task.taskCompletedDate + }); + } + } + + // Format the response + const response = { + caseInstance: { + caseID: caseInstance.caseID, + caseUUID: caseInstance.caseUUID, + caseName: caseInstance.caseName, + caseStatus: caseInstance.caseStatus, + caseCreatedDate: caseInstance.caseCreatedDate, + caseModifiedDate: caseInstance.caseModifiedDate, + caseCompletedDate: caseInstance.caseCompletedDate, + caseVariables: caseInstance.caseVariables, + process: { + processID: caseInstance.process.processID, + processUUID: caseInstance.process.processUUID, + processName: caseInstance.process.processName, + processDescription: caseInstance.process.processDescription + }, + startedBy: caseInstance.startedBy ? { + userID: caseInstance.startedBy.userID, + userFullName: caseInstance.startedBy.userFullName, + userUsername: caseInstance.startedBy.userUsername + } : null + }, + forms: forms, + timeline: caseInstance.timeline + }; + + return { + success: true, + ...response + }; + } catch (error) { + console.error('Error fetching case instance and forms:', error); + + return { + success: false, + error: 'Failed to fetch case instance and forms', + details: process.env.NODE_ENV === 'development' ? error.message : undefined + }; + } +}); \ No newline at end of file diff --git a/server/api/process/[id]/start.post.js b/server/api/process/[id]/start.post.js new file mode 100644 index 0000000..1a2030c --- /dev/null +++ b/server/api/process/[id]/start.post.js @@ -0,0 +1,174 @@ +import { PrismaClient } from '@prisma/client'; +import { v4 as uuidv4 } from 'uuid'; + +// Initialize Prisma client +const prisma = new PrismaClient(); + +export default defineEventHandler(async (event) => { + try { + // Get the process ID from the route parameter + const processId = getRouterParam(event, 'id'); + + if (!processId) { + return { + success: false, + error: 'Process ID is required' + }; + } + + console.log('Starting process with ID:', processId); + + // Check if the ID is a UUID or numeric ID + const isUUID = processId.length === 36 && processId.includes('-'); + + // Find the process + console.log('Finding process...'); + const process = await prisma.process.findFirst({ + where: isUUID + ? { processUUID: processId } + : { processID: parseInt(processId) }, + include: { + creator: { + select: { + userID: true, + userFullName: true, + userUsername: true + } + } + } + }); + + if (!process) { + console.log('Process not found'); + return { + success: false, + error: 'Process not found' + }; + } + + console.log('Process found:', process.processName); + + // Check if process is published + if (process.processStatus !== 'published') { + console.log('Process is not published:', process.processStatus); + return { + success: false, + error: 'Cannot start an unpublished process' + }; + } + + // Get the current user (in a real app, this would come from the authenticated user) + const currentUser = { + userID: 1, // This would be the actual user ID in a real app + userFullName: 'John Doe', + userUsername: 'johndoe' + }; + + console.log('Creating case instance...'); + // Create a new case instance + const caseInstance = await prisma.caseInstance.create({ + data: { + caseUUID: uuidv4(), + processID: process.processID, + caseName: `${process.processName} - ${new Date().toLocaleDateString()}`, + caseStatus: 'active', + caseStartedBy: currentUser.userID, + caseVariables: process.processVariables || {}, + caseSettings: process.processSettings || {}, + caseDefinition: process.processDefinition || {}, + caseCreatedDate: new Date(), + caseModifiedDate: new Date() + } + }); + + console.log('Case instance created:', caseInstance.caseUUID); + + // Get the process definition + const processDefinition = process.processDefinition || {}; + const nodes = processDefinition.nodes || []; + const edges = processDefinition.edges || []; + + // Find all form nodes + const formNodes = nodes.filter(node => node.type === 'form'); + + if (formNodes.length === 0) { + console.log('No form nodes found in process'); + return { + success: false, + error: 'Process does not contain any forms' + }; + } + + console.log(`Found ${formNodes.length} form nodes`); + + // Create tasks for all forms + const tasks = []; + for (const formNode of formNodes) { + console.log('Creating task for form:', formNode.data?.label); + const task = await prisma.task.create({ + data: { + taskUUID: uuidv4(), + caseID: caseInstance.caseID, + taskName: formNode.data?.label || 'Complete Form', + taskType: 'form', + taskStatus: 'pending', + taskAssignedTo: currentUser.userID, + taskFormID: formNode.data?.formId, + taskCreatedDate: new Date(), + taskModifiedDate: new Date() + } + }); + tasks.push(task); + console.log('Task created:', task.taskUUID); + } + + // Add to case timeline + console.log('Adding to case timeline...'); + await prisma.caseTimeline.create({ + data: { + caseID: caseInstance.caseID, + timelineType: 'start', + timelineDescription: `Process started by ${currentUser.userFullName}`, + timelineDate: new Date(), + timelineCreatedBy: currentUser.userID + } + }); + + console.log('Process started successfully'); + return { + success: true, + data: { + case: { + id: caseInstance.caseUUID, + name: caseInstance.caseName, + status: caseInstance.caseStatus, + startedAt: caseInstance.caseCreatedDate + }, + tasks: tasks.map(task => ({ + id: task.taskUUID, + name: task.taskName, + type: task.taskType, + formId: task.taskFormID, + status: task.taskStatus + })) + } + }; + } catch (error) { + console.error('Error starting process:', error); + console.error('Error details:', { + name: error.name, + message: error.message, + stack: error.stack, + code: error.code + }); + + return { + success: false, + error: 'Failed to start process', + details: process.env.NODE_ENV === 'development' ? error.message : undefined + }; + } finally { + // Close the Prisma client connection + await prisma.$disconnect(); + } +}); \ No newline at end of file diff --git a/server/api/process/pending.get.js b/server/api/process/pending.get.js new file mode 100644 index 0000000..759ddc8 --- /dev/null +++ b/server/api/process/pending.get.js @@ -0,0 +1,41 @@ +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +export default defineEventHandler(async (event) => { + try { + // Get all published processes that haven't been started + const processes = await prisma.process.findMany({ + where: { + status: 'published', + cases: { + none: { + status: 'active' + } + } + }, + select: { + id: true, + name: true, + description: true, + status: true, + createdAt: true, + updatedAt: true + }, + orderBy: { + updatedAt: 'desc' + } + }); + + return { + success: true, + data: processes + }; + } catch (error) { + console.error('Error fetching pending processes:', error); + return { + success: false, + error: 'Failed to fetch pending processes' + }; + } +}); \ No newline at end of file diff --git a/server/api/tasks/[id].get.js b/server/api/tasks/[id].get.js new file mode 100644 index 0000000..8e54ac9 --- /dev/null +++ b/server/api/tasks/[id].get.js @@ -0,0 +1,122 @@ +import { PrismaClient } from '@prisma/client'; + +// Initialize Prisma client +const prisma = new PrismaClient(); + +export default defineEventHandler(async (event) => { + // Get the task ID from the route params + const taskId = event.context.params.id; + + try { + // Find the task + let task; + + if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(taskId)) { + // If it looks like a UUID + task = await prisma.task.findUnique({ + where: { taskUUID: taskId }, + include: { + case: { + select: { + caseID: true, + caseUUID: true, + caseName: true, + caseStatus: true, + process: { + select: { + processID: true, + processUUID: true, + processName: true + } + } + } + }, + assignedTo: { + select: { + userID: true, + userFullName: true, + userUsername: true + } + }, + form: { + select: { + formID: true, + formUUID: true, + formName: true, + formDescription: true, + formComponents: true, + formStatus: true, + customCSS: true, + customScript: true, + formEvents: true, + scriptMode: true + } + } + } + }); + } else if (!isNaN(parseInt(taskId))) { + // If it's a numeric ID + task = await prisma.task.findUnique({ + where: { taskID: parseInt(taskId) }, + include: { + case: { + select: { + caseID: true, + caseUUID: true, + caseName: true, + caseStatus: true, + process: { + select: { + processID: true, + processUUID: true, + processName: true + } + } + } + }, + assignedTo: { + select: { + userID: true, + userFullName: true, + userUsername: true + } + }, + form: { + select: { + formID: true, + formUUID: true, + formName: true, + formDescription: true, + formComponents: true, + formStatus: true, + customCSS: true, + customScript: true, + formEvents: true, + scriptMode: true + } + } + } + }); + } + + if (!task) { + return { + success: false, + error: 'Task not found' + }; + } + + return { + success: true, + task + }; + } catch (error) { + console.error(`Error fetching task ${taskId}:`, error); + + return { + success: false, + error: 'Failed to fetch task', + details: process.env.NODE_ENV === 'development' ? error.message : undefined + }; + } +}); \ No newline at end of file diff --git a/server/api/tasks/[id]/save-draft.post.js b/server/api/tasks/[id]/save-draft.post.js new file mode 100644 index 0000000..ba99801 --- /dev/null +++ b/server/api/tasks/[id]/save-draft.post.js @@ -0,0 +1,72 @@ +import { PrismaClient } from '@prisma/client'; + +// Initialize Prisma client +const prisma = new PrismaClient(); + +export default defineEventHandler(async (event) => { + // Get the task ID from the route params + const taskId = event.context.params.id; + + try { + // Parse the request body + const body = await readBody(event); + + // Validate required fields + if (!body.formData) { + return { + success: false, + error: 'Form data is required' + }; + } + + // Find the task + let task; + + if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(taskId)) { + // If it looks like a UUID + task = await prisma.task.findUnique({ + where: { taskUUID: taskId } + }); + } else if (!isNaN(parseInt(taskId))) { + // If it's a numeric ID + task = await prisma.task.findUnique({ + where: { taskID: parseInt(taskId) } + }); + } + + if (!task) { + return { + success: false, + error: 'Task not found' + }; + } + + // Update the task with the draft form data + const updatedTask = await prisma.task.update({ + where: { + taskID: task.taskID + }, + data: { + taskData: { + ...task.taskData, + formData: body.formData, + lastSaved: new Date().toISOString(), + isDraft: true + } + } + }); + + return { + success: true, + task: updatedTask + }; + } catch (error) { + console.error(`Error saving draft for task ${taskId}:`, error); + + return { + success: false, + error: 'Failed to save draft', + details: process.env.NODE_ENV === 'development' ? error.message : undefined + }; + } +}); \ No newline at end of file diff --git a/server/api/tasks/[id]/submit.post.js b/server/api/tasks/[id]/submit.post.js new file mode 100644 index 0000000..7ceb0dd --- /dev/null +++ b/server/api/tasks/[id]/submit.post.js @@ -0,0 +1,122 @@ +import { PrismaClient } from '@prisma/client'; + +// Initialize Prisma client +const prisma = new PrismaClient(); + +export default defineEventHandler(async (event) => { + // Get the task ID from the route params + const taskId = event.context.params.id; + + try { + // Parse the request body + const body = await readBody(event); + + // Validate required fields + if (!body.formData) { + return { + success: false, + error: 'Form data is required' + }; + } + + // Find the task + let task; + + if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(taskId)) { + // If it looks like a UUID + task = await prisma.task.findUnique({ + where: { taskUUID: taskId }, + include: { + case: true + } + }); + } else if (!isNaN(parseInt(taskId))) { + // If it's a numeric ID + task = await prisma.task.findUnique({ + where: { taskID: parseInt(taskId) }, + include: { + case: true + } + }); + } + + if (!task) { + return { + success: false, + error: 'Task not found' + }; + } + + // Update the task with the submitted form data + const updatedTask = await prisma.task.update({ + where: { + taskID: task.taskID + }, + data: { + taskStatus: 'completed', + taskCompletedDate: new Date(), + taskData: { + ...task.taskData, + formData: body.formData, + submittedAt: new Date().toISOString() + } + } + }); + + // Add an entry to the case timeline + await prisma.caseTimeline.create({ + data: { + caseID: task.caseID, + timelineType: 'task_completed', + timelineDescription: `Task "${task.taskName}" was completed`, + timelineCreatedBy: task.taskAssignedTo + } + }); + + // Check if all tasks for the case are completed + const remainingTasks = await prisma.task.count({ + where: { + caseID: task.caseID, + taskStatus: { + not: 'completed' + } + } + }); + + // If all tasks are completed, update the case status + if (remainingTasks === 0) { + await prisma.caseInstance.update({ + where: { + caseID: task.caseID + }, + data: { + caseStatus: 'completed', + caseCompletedDate: new Date() + } + }); + + // Add an entry to the case timeline + await prisma.caseTimeline.create({ + data: { + caseID: task.caseID, + timelineType: 'case_completed', + timelineDescription: `Case "${task.case.caseName}" was completed`, + timelineCreatedBy: task.taskAssignedTo + } + }); + } + + return { + success: true, + task: updatedTask + }; + } catch (error) { + console.error(`Error submitting form for task ${taskId}:`, error); + + return { + success: false, + error: 'Failed to submit form', + details: process.env.NODE_ENV === 'development' ? error.message : undefined + }; + } +}); \ No newline at end of file diff --git a/server/api/tasks/index.get.js b/server/api/tasks/index.get.js new file mode 100644 index 0000000..759a0a4 --- /dev/null +++ b/server/api/tasks/index.get.js @@ -0,0 +1,96 @@ +import { PrismaClient } from '@prisma/client'; + +// Initialize Prisma client +const prisma = new PrismaClient(); + +export default defineEventHandler(async (event) => { + try { + // Get query parameters + const query = getQuery(event); + const caseId = query.caseId; + const status = query.status; + const assignedTo = query.assignedTo; + const page = parseInt(query.page) || 1; + const limit = parseInt(query.limit) || 10; + const skip = (page - 1) * limit; + + // Build where clause + const where = {}; + + if (caseId) { + where.caseID = parseInt(caseId); + } + + if (status) { + where.taskStatus = status; + } + + if (assignedTo) { + where.taskAssignedTo = parseInt(assignedTo); + } + + // Fetch tasks + const tasks = await prisma.task.findMany({ + where, + include: { + case: { + select: { + caseID: true, + caseUUID: true, + caseName: true, + caseStatus: true, + process: { + select: { + processID: true, + processUUID: true, + processName: true + } + } + } + }, + assignedTo: { + select: { + userID: true, + userFullName: true, + userUsername: true + } + }, + form: { + select: { + formID: true, + formUUID: true, + formName: true, + formDescription: true + } + } + }, + orderBy: { + taskCreatedDate: 'desc' + }, + skip, + take: limit + }); + + // Count total tasks for pagination + const totalTasks = await prisma.task.count({ where }); + + return { + success: true, + tasks, + pagination: { + page, + limit, + totalItems: totalTasks, + totalPages: Math.ceil(totalTasks / limit) + } + }; + } catch (error) { + console.error('Error fetching tasks:', error); + + return { + success: false, + error: 'Failed to fetch tasks', + details: process.env.NODE_ENV === 'development' ? error.message : undefined + }; + } +}); \ No newline at end of file diff --git a/server/middleware/dynamic-routes.js b/server/middleware/dynamic-routes.js new file mode 100644 index 0000000..dde552e --- /dev/null +++ b/server/middleware/dynamic-routes.js @@ -0,0 +1,9 @@ +export default defineEventHandler((event) => { + const url = event.node.req.url; + + // Check if the URL contains literal [id] which should be a parameter + if (url && url.includes('/execution/form/[id]')) { + // Redirect to a more appropriate error page or handle differently + return sendRedirect(event, '/execution', 302); + } +}); \ No newline at end of file