Chào bạn,
Ở bài học trước, chúng ta đã xây dựng thành công một trang web đơn giản để truy vấn và hiển thị danh sách các User Task từ Camunda engine. Giao diện của bạn giờ đây đã có thể "nhìn thấy" công việc, nhưng chưa thể "làm" được.
Trong bài học hôm nay, "Tích hợp chức năng claim và complete task vào giao diện web tùy chỉnh", chúng ta sẽ nâng cấp trang tasklist.html để nó trở nên tương tác thực sự. Bạn sẽ học cách thêm các nút "Claim" (Nhận việc) và "Complete" (Hoàn thành) và viết logic JavaScript để gọi đến các endpoint REST API tương ứng của Camunda. Sau bài học này, người dùng sẽ có thể quản lý vòng đời cơ bản của một công việc—nhận quyền sở hữu và hoàn thành nó—ngay trên giao diện web tùy chỉnh của bạn.
1. Vòng đời của một User Task: Claim và Complete
Trước khi đi vào code, hãy cùng xem lại quy trình làm việc chuẩn với một User Task:
- Unassigned (Chưa được giao): Một task được tạo ra và có thể được nhìn thấy bởi một nhóm người dùng (
candidateGroup) hoặc một số người dùng cụ thể (candidateUsers). Bất kỳ ai trong nhóm này đều có thể nhận việc. - Claiming (Nhận việc): Một người dùng quyết định thực hiện task này. Họ "claim" task đó. Tại thời điểm này, task được gán cho duy nhất người dùng đó (
assignee) và biến mất khỏi danh sách chờ của những người khác. - Assigned (Đã được giao): Task đang thuộc sở hữu của một người dùng và họ chịu trách nhiệm hoàn thành nó.
- Completing (Hoàn thành): Sau khi hoàn thành công việc, người dùng "complete" task. Hành động này báo cho Camunda engine rằng bước này đã xong và quy trình có thể tiếp tục di chuyển đến các bước tiếp theo.

Để thực hiện các hành động này, chúng ta cần tương tác với hai endpoint quan trọng của Camunda REST API.
2. Thực hành: Tích hợp chức năng "Claim Task"
Chức năng "Claim" cho phép một người dùng nhận quyền sở hữu một task chưa được giao.
API để Claim Task
Để claim một task, chúng ta gửi một yêu cầu POST đến endpoint sau:
POST /task/{id}/claim
Trong đó {id} là ID của task. Request body cần chứa userId của người sẽ nhận task.
{
"userId": "demo"
}
Bây giờ, hãy cập nhật file tasklist.html của bạn.
Bước 1: Thêm nút "Claim" vào bảng
Chúng ta sẽ sửa đổi hàm renderTasks để hiển thị một nút "Claim" nếu task đó chưa có ai nhận (assignee là null), và thêm một cột "Actions" vào bảng.
Mở tasklist.html và cập nhật phần HTML và JavaScript.
Cập nhật HTML (thêm cột Actions):
...
<table>
<thead>
<tr>
<th>Task ID</th>
<th>Task Name</th>
<th>Assignee</th>
<th>Created Date</th>
<th>Actions</th> <!-- CỘT MỚI -->
</tr>
</thead>
<tbody id="task-list-body">
<!-- Dữ liệu task sẽ được chèn vào đây -->
</tbody>
</table>
...
Cập nhật JavaScript (hàm renderTasks và thêm hàm claimTask):
// Thêm biến này ở đầu thẻ <script>
const currentUser = 'demo'; // Giả lập người dùng hiện tại là 'demo'
// ... (giữ nguyên hàm fetchTasks)
// CẬP NHẬT HÀM NÀY
function renderTasks(tasks) {
taskListBody.innerHTML = '';
if (tasks.length === 0) {
taskListBody.innerHTML = '<tr><td colspan="5">No tasks found.</td></tr>';
return;
}
tasks.forEach(task => {
const row = document.createElement('tr');
// Tạo nội dung cho các ô
let actionsCell = '';
if (task.assignee === null) {
// Nếu task chưa được giao, hiển thị nút Claim
actionsCell = `<button onclick="claimTask('${task.id}')">Claim</button>`;
} else if (task.assignee === currentUser) {
// Tạm thời để trống, chúng ta sẽ thêm nút Complete ở phần sau
actionsCell = ``;
} else {
actionsCell = 'Assigned to other';
}
row.innerHTML = `
<td>${task.id}</td>
<td>${task.name}</td>
<td>${task.assignee || 'Unassigned'}</td>
<td>${new Date(task.created).toLocaleString()}</td>
<td>${actionsCell}</td>
`;
taskListBody.appendChild(row);
});
}
// THÊM HÀM MỚI
async function claimTask(taskId) {
console.log(`Claiming task: ${taskId}`);
try {
const response = await fetch(`${camundaApiUrl}/task/${taskId}/claim`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ userId: currentUser })
});
if (!response.ok) {
// Camunda trả về 204 No Content khi thành công, nên không cần check response.json()
if (response.status !== 204) {
throw new Error(`HTTP error! status: ${response.status}`);
}
}
console.log(`Task ${taskId} claimed successfully.`);
fetchTasks(); // Tải lại danh sách task để cập nhật giao diện
} catch (error) {
console.error('Could not claim task:', error);
alert('Failed to claim task. See console for details.');
}
}
Chạy thử:
- Khởi động lại ứng dụng Camunda của bạn.
- Tạo một vài process instance để có các User Task chưa được gán cho một
candidateGrouphoặccandidateUser(ví dụ: nhómmanagementnhư bài trước). - Mở
tasklist.html. Bạn sẽ thấy các task "Unassigned" có nút "Claim". - Nhấn nút "Claim". Task đó sẽ được gán cho người dùng
demovà nút "Claim" sẽ biến mất.
3. Thực hành: Tích hợp chức năng "Complete Task"
Sau khi một task đã được claim và thực hiện xong, người dùng cần hoàn thành nó.
API để Complete Task
Để hoàn thành một task, chúng ta gửi một yêu cầu POST đến endpoint:
POST /task/{id}/complete
Request body có thể rỗng, hoặc chứa các biến quy trình (process variables) mà bạn muốn cập nhật hoặc tạo mới. Đây là cách giao diện người dùng gửi dữ liệu trở lại quy trình, ví dụ để quyết định hướng đi tiếp theo tại một gateway.
{
"variables": {
"approved": { "value": true, "type": "Boolean" },
"comments": { "value": "Looks good.", "type": "String" }
}
}
Để hiểu rõ hơn về cách gọi API này, hãy xem một đoạn video ngắn từ Camunda. Video sử dụng Postman, nhưng các khái niệm về endpoint, phương thức, và body của request là hoàn toàn giống với những gì chúng ta đang làm với JavaScript.
Tutorial: How to Use Camunda's Rest API (Video 4)
Video này của Camunda sẽ minh họa rõ ràng cách gọi API để hoàn thành một task và truyền biến vào quy trình. Hãy chú ý cách họ xây dựng request body.
Hãy xem từ 15:56 đến 19:17. Bạn sẽ thấy cách sử dụng endpoint POST /task/{id}/complete với một JSON body chứa các biến (variables). Đây chính xác là những gì chúng ta sắp implement.
Bước 2: Thêm nút "Complete" vào bảng
Bây giờ, chúng ta sẽ hoàn thiện hàm renderTasks để hiển thị nút "Complete" cho các task đã được gán cho người dùng hiện tại.
Cập nhật JavaScript (hàm renderTasks và thêm hàm completeTask):
// ...
// CẬP NHẬT HOÀN CHỈNH HÀM NÀY
function renderTasks(tasks) {
taskListBody.innerHTML = '';
if (tasks.length === 0) {
taskListBody.innerHTML = '<tr><td colspan="5">No tasks found.</td></tr>';
return;
}
tasks.forEach(task => {
const row = document.createElement('tr');
let actionsCell = '';
if (task.assignee === null) {
// Nếu task chưa được giao, hiển thị nút Claim
actionsCell = `<button onclick="claimTask('${task.id}')">Claim</button>`;
} else if (task.assignee === currentUser) {
// Nếu task được giao cho user hiện tại, hiển thị nút Complete
actionsCell = `<button onclick="completeTask('${task.id}')">Complete</button>`;
} else {
actionsCell = `Assigned to ${task.assignee}`;
}
row.innerHTML = `
<td>${task.id}</td>
<td>${task.name}</td>
<td>${task.assignee || 'Unassigned'}</td>
<td>${new Date(task.created).toLocaleString()}</td>
<td>${actionsCell}</td>
`;
taskListBody.appendChild(row);
});
}
// ... (giữ nguyên hàm claimTask)
// THÊM HÀM MỚI
async function completeTask(taskId) {
console.log(`Completing task: ${taskId}`);
// Ví dụ: Hoàn thành task và gửi kèm một biến 'taskOutcome'
const variables = {
variables: {
taskOutcome: {
value: "Approved",
type: "String"
}
}
};
try {
const response = await fetch(`${camundaApiUrl}/task/${taskId}/complete`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(variables) // Gửi kèm biến
// body: JSON.stringify({}) // Hoặc gửi body rỗng nếu không cần truyền biến
});
if (!response.ok) {
if (response.status !== 204) {
throw new Error(`HTTP error! status: ${response.status}`);
}
}
console.log(`Task ${taskId} completed successfully.`);
fetchTasks(); // Tải lại danh sách, task đã hoàn thành sẽ biến mất
} catch (error) {
console.error('Could not complete task:', error);
alert('Failed to complete task. See console for details.');
}
}
Chạy thử:
- Refresh trang
tasklist.htmlcủa bạn. - Claim một task nếu bạn chưa làm. Nút "Claim" sẽ được thay thế bằng nút "Complete".
- Nhấn nút "Complete". Task đó sẽ được hoàn thành và biến mất khỏi danh sách.
- Bạn có thể vào Camunda Cockpit để xem process instance tương ứng đã di chuyển đến bước tiếp theo, và biến
taskOutcomeđã được lưu lại.
Tổng kết
Xin chúc mừng! Giao diện quản lý tác vụ của bạn không còn chỉ ở chế độ "chỉ đọc". Trong bài học này, bạn đã:
- Hiểu rõ vòng đời
Claim->Completecủa một User Task. - Sử dụng Camunda REST API để thực hiện hai hành động quan trọng:
POST /task/{id}/claimđể nhận quyền sở hữu một task.POST /task/{id}/completeđể hoàn thành một task và gửi dữ liệu trở lại quy trình.
- Tích hợp thành công các hành động này vào giao diện web tùy chỉnh, tạo ra một ứng dụng tương tác thực sự.
Bạn đã xây dựng được một vòng lặp hoàn chỉnh: Camunda engine tạo ra công việc, giao diện của bạn hiển thị công việc đó, người dùng tương tác để nhận và hoàn thành nó, và kết quả được gửi trở lại engine để tiếp tục quy trình.
Xem trước bài học tiếp theo
Hiện tại, việc hoàn thành task của chúng ta đang gửi đi dữ liệu được hard-code (taskOutcome: "Approved"). Trong thực tế, người dùng cần nhập dữ liệu thông qua các ô văn bản, checkbox, dropdown...
Trong bài học tiếp theo, "Tạo Form cho User Tasks sử dụng Camunda Forms", chúng ta sẽ tìm hiểu cách thiết kế các form nhập liệu một cách trực quan ngay trong Camunda Modeler. Những form này sẽ được tự động hiển thị cho người dùng, giúp việc thu thập dữ liệu khi hoàn thành task trở nên dễ dàng và có cấu trúc hơn rất nhiều.