1. Mở đầu

Trong công việc IT, dữ liệu về bug, incident và yêu cầu hỗ trợ thường được lưu trong bảng tính hoặc các công cụ quản lý ticket. Dữ liệu có thể đầy đủ, nhưng một danh sách nhiều dòng không giúp người xem nhanh chóng trả lời những câu hỏi như: còn bao nhiêu ticket chưa xử lý, service nào đang gặp nhiều vấn đề, tỷ lệ đáp ứng SLA là bao nhiêu và ticket nào cần được ưu tiên trước.

Trong bài viết này, tôi thử xây dựng một dashboard IT Ticket & SLA bằng Google Apps Script (GAS) HTML Service. Google Sheets được sử dụng làm nguồn dữ liệu, Apps Script đảm nhiệm việc đọc và tổng hợp, còn trang HTML hiển thị KPI, biểu đồ, bộ lọc và bảng ticket ưu tiên. Phần thiết kế được thực hiện theo các nguyên tắc trong Dashboard Design Practice Guidebook của Digital Agency Nhật Bản.

Tôi chọn chủ đề ticket vì nó gần với quy trình làm việc của một đội IT. Trong daily stand-up hoặc bug triage, thành viên trong đội không chỉ cần một báo cáo tổng hợp mà còn phải xác định được vấn đề nào cần hành động ngay. Demo sử dụng 40 ticket giả lập, không chứa dữ liệu của một hệ thống hoặc doanh nghiệp thật.

2. Tổng quan về dashboard bằng GAS HTML Service

Google Apps Script là công cụ cho phép tạo các chương trình nhỏ hoạt động cùng Google Sheets và những sản phẩm khác trong Google Workspace. HTML Service giúp chương trình đó hiển thị một trang web, vì vậy dashboard có thể được mở bằng một đường dẫn trên trình duyệt thay vì chỉ xem dữ liệu trực tiếp trong bảng tính.

Trong demo này, Google Sheets là nơi lưu dữ liệu ticket. Khi người dùng mở Web App, Google Apps Script gửi giao diện dashboard từ Index.html về trình duyệt. Giao diện yêu cầu Code.gs đọc và tổng hợp dữ liệu trong sheet Tickets.

Sau khi nhận yêu cầu, Code.gs tính các chỉ số như tổng ticket, số ticket chưa đóng, tỷ lệ đạt SLA và thời gian xử lý trung bình. Kết quả được gửi lại cho Index.html để hiển thị thành KPI, biểu đồ và bảng ticket ưu tiên. Khi người dùng thay đổi bộ lọc, quá trình này được thực hiện lại với các điều kiện mới.

Kiến trúc tổng thể như sau:

Google Sheets lưu dữ liệu ticket
              ↓
Code.gs đọc, lọc và tính toán
              ↓
Index.html hiển thị dashboard
              ↓
Người dùng xem và lọc dữ liệu

Như vậy, ba phần có vai trò tách biệt: Google Sheets lưu dữ liệu, Code.gs xử lý dữ liệu và Index.html trình bày kết quả. Người đọc không cần biết chi tiết code để hiểu dashboard hoạt động như thế nào.

GAS HTML Service phù hợp với prototype và công cụ nội bộ quy mô nhỏ vì không cần tự vận hành web server hoặc database riêng. Khi dữ liệu đã nằm trong Google Sheets, thời gian để tạo một Web App có thể ngắn hơn so với xây một hệ thống frontend và backend hoàn chỉnh. Tuy nhiên, GAS có quota thực thi, Google Sheets không phải kho dữ liệu lớn và quyền truy cập Web App cần được cấu hình cẩn thận.

3. Những nguyên tắc chính từ guidebook của Digital Agency

Digital Agency công bố guidebook với mục tiêu giúp cơ quan công, doanh nghiệp và cá nhân xây dựng dashboard dễ hiểu, từ đó tạo nhận thức chung và nâng chất lượng quyết định dựa trên dữ liệu. Tài liệu không chỉ nói về màu sắc hoặc loại biểu đồ mà còn mô tả quá trình từ làm rõ yêu cầu, tạo prototype cho tới triển khai và cải tiến.

Bắt đầu từ người xem và hành động

Nguyên tắc đầu tiên là xác định dashboard được tạo cho ai, được xem khi nào, người xem muốn biết điều gì và họ sẽ hành động như thế nào sau khi xem. Guidebook gợi ý sử dụng các câu hỏi 5W1H để làm rõ mục tiêu và bối cảnh sử dụng.

Với demo này, người xem là thành viên hoặc quản lý đội IT. Dashboard có thể được mở trước daily stand-up, bug triage hoặc buổi đánh giá vận hành. Sau khi xem, đội cần nhận biết ticket Critical/High, điều chỉnh nguồn lực và điều tra service đang phát sinh nhiều sự cố.

Từ mục tiêu đó, tôi xác định bốn câu hỏi nghiệp vụ trước khi chọn biểu đồ:

  1. Trong kỳ có bao nhiêu ticket và còn bao nhiêu ticket chưa đóng?
  2. Tỷ lệ đáp ứng SLA và thời gian xử lý trung bình là bao nhiêu?
  3. Ticket tập trung ở service và trạng thái nào?
  4. Ticket High/Critical nào cần được xử lý trước?

Làm rõ yêu cầu, prototype rồi mới triển khai

Guidebook tổ chức quá trình xây dashboard thành ba chặng lớn: sắp xếp yêu cầu và điều kiện ràng buộc, tạo prototype để lấy phản hồi, sau đó triển khai và tiếp tục cải tiến. Cách làm này giúp tránh việc dành nhiều thời gian cho màu sắc hoặc hiệu ứng khi cấu trúc thông tin vẫn chưa được thống nhất.

Tôi áp dụng bằng cách khóa trước bố cục gồm bốn KPI, ba biểu đồ và một bảng ticket ưu tiên. Dữ liệu giả được sử dụng để kiểm tra công thức và luồng đọc trước khi kết nối dữ liệu thật. Prototype cũng giúp stakeholder thống nhất định nghĩa “ticket chưa đóng” hoặc “đạt SLA”, vốn là những khái niệm có thể được hiểu khác nhau giữa các đội.

Trình bày từ tổng quan đến chi tiết

Thông tin trên dashboard cần có cấu trúc phân cấp. Người xem nên thấy bức tranh tổng thể trước, sau đó mới đi vào các nhóm và bản ghi chi tiết. Vì vậy, dashboard đặt KPI ngay dưới bộ lọc, tiếp theo là biểu đồ xu hướng và so sánh, cuối cùng là bảng ticket cần hành động.

Luồng đọc được thiết kế từ trên xuống dưới:

Tiêu đề, mục đích và thời điểm tạo báo cáo
Bộ lọc ngày, service và priority
4 KPI tổng quan
Biểu đồ xu hướng theo ngày
Biểu đồ theo service và trạng thái
Bảng ticket High/Critical chưa đóng
Nguồn dữ liệu và định nghĩa chỉ số

Chọn biểu đồ theo loại câu hỏi

Mỗi loại biểu đồ có một mục đích khác nhau. Biểu đồ đường phù hợp với thay đổi theo thời gian, trong khi biểu đồ cột hoặc thanh phù hợp với việc so sánh số lượng giữa các nhóm. Bảng dữ liệu phù hợp khi người xem cần đọc chính xác từng bản ghi.

Câu hỏi Cách trình bày Lý do
Ticket thay đổi theo ngày thế nào? Biểu đồ đường Thể hiện xu hướng thời gian
Service nào có nhiều ticket? Biểu đồ thanh ngang So sánh số lượng giữa các service
Ticket đang ở trạng thái nào? Biểu đồ cột So sánh Open, In Progress và Resolved
Ticket nào cần hành động? Bảng dữ liệu Hiển thị chính xác từng ticket

Tôi không sử dụng biểu đồ tròn vì mục tiêu chính là so sánh chính xác số lượng giữa các nhóm, không phải thể hiện một cơ cấu đơn giản.

Giúp người xem biết điều cần biết và không gây hiểu nhầm

Guidebook nhấn mạnh hai mục tiêu lớn của thiết kế biểu đồ: người xem tìm được thông tin họ cần và cách trình bày không làm dữ liệu bị hiểu sai. Trong demo, service được xếp theo số ticket giảm dần. Bảng ưu tiên xếp Critical trước High, sau đó ticket cũ trước ticket mới. Trục của biểu đồ cột và thanh bắt đầu từ 0 để không phóng đại chênh lệch, đồng thời không sử dụng hiệu ứng 3D hoặc trang trí không mang thông tin.

Dashboard cũng hiển thị kỳ dữ liệu, nguồn, thời điểm tạo báo cáo và định nghĩa chỉ số. Nếu không có ticket đã xác định SLA, giao diện hiển thị dấu  thay vì 0%, bởi “chưa có dữ liệu để tính” không giống với “không ticket nào đạt SLA”. Khi bộ lọc không có kết quả, dashboard hiển thị empty state thay vì để lại một vùng biểu đồ trống khó hiểu.

Màu sắc và khả năng tiếp cận

Màu xanh được dùng làm màu chủ đạo, còn đỏ và cam chỉ dành cho cảnh báo. Tuy nhiên, màu không phải phương tiện duy nhất truyền đạt trạng thái. Badge luôn hiển thị cả chữ Critical hoặc High; các biểu đồ có tiêu đề và nhãn; bảng dữ liệu cung cấp cách đọc bằng văn bản.

Giao diện sử dụng label cho input, vùng thông báo aria-live, caption và header cho bảng, focus outline cho thao tác bàn phím và bố cục responsive trên màn hình hẹp. Nếu triển khai cho dịch vụ công hoặc sử dụng rộng rãi, dashboard cần được kiểm tra tương phản bằng công cụ chuyên dụng và bổ sung mô tả dữ liệu đầy đủ hơn cho người sử dụng screen reader.

Các nguyên tắc trên là cơ sở để tôi xác định dữ liệu, KPI và bố cục trước khi yêu cầu AI tạo code.

4. Thiết kế dashboard mẫu

Câu hỏi nghiệp vụ

Dashboard được thiết kế để trả lời bốn câu hỏi:

  1. Trong kỳ có bao nhiêu ticket và còn bao nhiêu ticket chưa đóng?
  2. Tỷ lệ đáp ứng SLA và thời gian xử lý trung bình là bao nhiêu?
  3. Ticket tập trung ở service và trạng thái nào?
  4. Ticket High/Critical nào cần được xử lý trước?

Cấu trúc dữ liệu

Tập dữ liệu mẫu gồm 40 ticket trong khoảng từ ngày 25/08/2026 đến 13/09/2026. Mỗi ticket sử dụng mười trường:

ticket_id, created_at, service, category, priority,
status, assignee_team, resolved_at, resolution_hours, sla_met

Dữ liệu có 24 ticket Resolved, 9 ticket Open và 7 ticket In Progress. Bốn service gồm Payment API, Customer Portal, Mobile App và Internal Network. Dữ liệu được phân bố không đồng đều để biểu đồ so sánh có ý nghĩa; Payment API có 14 ticket và là service có nhiều ticket nhất.

Định nghĩa KPI

KPI Cách tính
Tổng ticket Số ticket sau khi áp dụng bộ lọc
Ticket chưa đóng Ticket có status khác Resolved
Tỷ lệ đạt SLA Yes / (Yes + No) × 100
Thời gian xử lý trung bình Trung bình resolution_hours của ticket Resolved

Nếu không có ticket đủ dữ liệu để tính SLA hoặc thời gian xử lý, KPI tương ứng sẽ trả về null và giao diện hiển thị dấu . Cách biểu diễn này giúp phân biệt “chưa có dữ liệu” với giá trị bằng 0.

Bố cục dashboard

Tiêu đề, mục đích và thời điểm tạo báo cáo
Bộ lọc ngày, service và priority
4 KPI tổng quan
Biểu đồ xu hướng theo ngày
Biểu đồ theo service và trạng thái
Bảng ticket High/Critical chưa đóng
Nguồn dữ liệu và định nghĩa chỉ số

Sau khi các yêu cầu này được xác định, tôi chuyển chúng thành prompt có cấu trúc để Codex tạo code.

5. Tạo code bằng Codex với GPT-5.6 Sol

Trong lần thực hành này, tôi sử dụng Codex với model GPT-5.6 Sol. Tôi mở một workspace dành cho demo rồi tách quá trình thành ba lượt: yêu cầu Codex tạo file backend Code.gs, tạo file giao diện Index.html, sau đó review chéo và sửa trực tiếp hai file. Việc chia nhỏ giúp mỗi prompt có phạm vi rõ và làm cho hợp đồng dữ liệu giữa backend với frontend dễ kiểm tra hơn.

Bài viết được trình bày bằng tiếng Việt, còn dashboard mẫu sử dụng tiếng Anh cho toàn bộ label, nút, trạng thái và thông báo lỗi. Số và ngày giờ trên giao diện được định dạng theo locale en-US.

Prompt tạo Code.gs

Đây là prompt tạo Code.gs:Xem prompt tạo Code.gs

You are a senior Google Apps Script developer working as a coding agent with access to the current workspace. Create or update a real file named `Code.gs` in the workspace. Do not merely show proposed code in the chat response.

The file must contain the complete backend for an IT Ticket & SLA dashboard. It must run on the Google Apps Script V8 runtime and be implemented as a container-bound script attached to a Google Sheet.

The data source is a sheet named exactly `Tickets`. Its header row contains:
ticket_id, created_at, service, category, priority, status, assignee_team, resolved_at, resolution_hours, sla_met

Mandatory requirements:
1. Create a `doGet()` function that returns the HTML file named `Index` through `HtmlService`. Set the page title to `IT Ticket & SLA Dashboard` and add a viewport meta tag.
2. Create a public function named `getDashboardData(filters)`. The filters object contains `startDate`, `endDate`, `service`, and `priority`. Date filtering must include both the start date and the end date.
3. Use `SpreadsheetApp.getActiveSpreadsheet()` and `getSheetByName('Tickets')`. Return clear English error messages when the active spreadsheet, sheet, or any required header is missing.
4. Accept date cells stored either as JavaScript `Date` objects or `YYYY-MM-DD` strings. The payload returned to the client must not contain any `Date` objects.
5. Normalize each row into a camelCase object. Skip invalid rows and count them in `skippedRows`. Valid priorities are Critical, High, Medium, and Low. Valid statuses are Open, In Progress, and Resolved.
6. A Resolved ticket must have a valid `resolved_at`, a non-negative numeric `resolution_hours`, and a `sla_met` value of Yes or No. The same three fields must be empty for tickets that are not Resolved.
7. Return an object with exactly this structure:
   - `kpis`: `{total, open, slaRate, avgResolutionHours}`
   - `trends`: an array of `{label, value}`, sorted by date in ascending order
   - `byService`: an array of `{label, value}`, sorted by value descending and then label ascending
   - `byStatus`: include Open, In Progress, and Resolved in that exact order, even when a value is 0
   - `urgentTickets`: unresolved Critical and High tickets, sorted by Critical before High and then oldest before newest; each item contains `ticketId`, `createdAt`, `service`, `category`, `priority`, `status`, and `assigneeTeam`
   - `options`: `{services, priorities}`, calculated before filters are applied
   - `meta`: `{periodStart, periodEnd, filteredCount, skippedRows, generatedAt}`
8. Define `open` as all tickets whose status is not Resolved. Calculate `slaRate` as `Yes / (Yes + No) * 100`, rounded to one decimal place, or `null` when no SLA data is available. Calculate `avgResolutionHours` as the average resolution time of Resolved tickets, rounded to one decimal place, or `null` when no value is available.
9. Use only JavaScript compatible with the Apps Script V8 runtime. Do not use npm, TypeScript, external APIs, or frameworks.
10. Split the logic into small, testable functions. Do not hard-code the sample dataset in `Code.gs`.

Before finishing:
1. Write the complete implementation to `Code.gs` in the current workspace.
2. Re-open the saved file and check all field names for consistency.
3. Perform any available syntax or static checks that do not require a live Google account.
4. Do not print the complete source code in the chat response.
5. In the final response, briefly report that `Code.gs` was created or updated, summarize its main functions, and state what still needs to be verified in the Apps Script Editor.

Tôi mở Codex trong workspace của demo, chọn model GPT-5.6 Sol rồi nhập nguyên prompt trên. Điểm quan trọng là prompt yêu cầu Codex sử dụng công cụ chỉnh sửa workspace để tạo file thật, thay vì chỉ in code trong câu trả lời.

Sau khi xử lý prompt, Codex tạo file Code.gs trực tiếp trong workspace. Tôi kiểm tra cây thư mục để xác nhận file đã được tạo thành công. Nội dung và vai trò của các hàm chính sẽ được trình bày bằng code snippet ở phần thực hành, vì vậy ảnh tại đây chỉ dùng để ghi lại kết quả prompt-to-file.

Hình 1. Code.gs phiên bản tiếng Anh trong workspace; cây thư mục bên trái hiển thị thêm Index.html, dữ liệu mẫu, README và các file kiểm tra của project.

Prompt tạo Index.html

Sau khi có file backend và payload contract, tôi dùng prompt sau để Codex đọc Code.gs rồi tạo file frontend trong cùng workspace:Xem prompt tạo Index.html

You are a senior frontend developer working as a coding agent with access to the current workspace. First inspect the existing `Code.gs` file to confirm the backend payload contract. Then create or update a real file named `Index.html` in the workspace. Do not merely show proposed HTML in the chat response.

The file must contain the complete interface for an `IT Ticket & SLA Dashboard` built for Google Apps Script HTML Service and accessible dashboard use.

Call the backend with `google.script.run.withSuccessHandler(...).withFailureHandler(...).getDashboardData(filters)`. The filters object contains `startDate`, `endDate`, `service`, and `priority`. The returned payload contains:
- `kpis`: `total`, `open`, `slaRate`, `avgResolutionHours`
- `trends`: an array of `{label, value}`, where `label` uses the `YYYY-MM-DD` format
- `byService` and `byStatus`: arrays of `{label, value}`
- `urgentTickets`: `ticketId`, `createdAt`, `service`, `category`, `priority`, `status`, `assigneeTeam`
- `options`: `services`, `priorities`
- `meta`: `periodStart`, `periodEnd`, `filteredCount`, `skippedRows`, `generatedAt`

Mandatory requirements:
1. Use a single HTML file with inline CSS and JavaScript. Do not use npm or any framework. You may load Google Charts from `https://www.gstatic.com/charts/loader.js` and use the `corechart` package.
2. All user-facing interface text must be in English. Set the document language to `en`. Use a light, responsive layout with blue as the primary color. Reserve red and orange for warnings. Below 760px, switch to a single-column layout.
3. The header must explain the dashboard's purpose and show when the report was generated.
4. Provide a filter form with start date, end date, service, and priority fields, plus `Apply` and `Reset` buttons. Validate that the start date is not later than the end date.
5. Display four KPIs: `total`, `open`, `slaRate`, and `avgResolutionHours`. Display `null` as `—` and format numbers with the `en-US` locale.
6. Use a `LineChart` for the daily trend with `dd/mm` labels, a `BarChart` for service comparison, and a `ColumnChart` for status comparison. Bar and column chart axes must start at 0. Do not use 3D effects. Each title must express the question answered by the chart.
7. Include an `urgentTickets` table. Priority must be communicated with both color and text. Build rows using `textContent` and `createElement`; never concatenate backend data into `innerHTML`.
8. Provide separate loading, error, empty, and dashboard states. Disable action buttons while loading. The error state must include a message and a `Try again` button.
9. Display the reporting period, the `Tickets` Google Sheet as the data source, the number of skipped rows, and the sample SLA definition.
10. Accessibility requirements: associate labels with inputs, provide `focus-visible` styles and an `aria-live` status region, use a caption and header cells for the table, and add `role="img"` and `aria-label` to chart containers. Never communicate status through color alone.
11. Redraw charts after window resize using debounce. Do not use continuous animation.
12. The code must run inside the Google Apps Script HTML Service sandbox and must not use ES modules.

Before finishing:
1. Write the complete implementation to `Index.html` in the current workspace.
2. Re-open both `Code.gs` and `Index.html` and verify that all DOM IDs and payload field names are consistent.
3. Perform any available HTML and JavaScript syntax or static checks that do not require a live Google account.
4. Do not print the complete HTML source in the chat response.
5. In the final response, briefly report that `Index.html` was created or updated, summarize the interface, and state what still needs to be verified after deployment to Google Apps Script.

Tiếp theo, tôi gửi prompt frontend trong cùng workspace để Codex có thể đọc Code.gs. Sau khi hoàn thành, file Index.html xuất hiện bên cạnh Code.gs; file này chứa HTML, CSS và JavaScript nội tuyến của dashboard.

Hình 2. File Index.html được tạo bên cạnh Code.gs; phần đầu file xác nhận giao diện sử dụng lang=”en”, tiêu đề tiếng Anh và thư viện Google Charts.

Prompt review hai file

Code do AI sinh ra không được mặc định xem là đúng. Tôi tiếp tục dùng prompt dưới đây để kiểm tra cú pháp GAS, hợp đồng dữ liệu, công thức KPI, trạng thái lỗi, an toàn DOM và mức độ tuân thủ guideline.Xem prompt review Code.gs và Index.html

You are reviewing a Google Apps Script Web App as a coding agent with access to the current workspace. Open and inspect the existing `Code.gs` and `Index.html` files for the IT Ticket & SLA dashboard. Make necessary corrections directly in those files instead of returning replacement files in the chat response.

Review them in this order:
1. Check syntax and compatibility with the Google Apps Script V8 runtime and HTML Service.
2. Compare every field name in the `Code.gs` payload with the corresponding field read by `Index.html`.
3. Verify the formulas for `total`, `open`, `slaRate`, and `avgResolutionHours`. Date filtering must include both boundary dates.
4. Check the handling of missing sheets or headers, invalid rows, loading/error/empty states, and invalid date ranges.
5. Check DOM safety: ticket data must never be concatenated directly into `innerHTML`.
6. Check the dashboard design principles: overview before detail, chart type appropriate to each question, bar and column axes starting at 0, no 3D effects, clear metadata, status not communicated by color alone, responsive layout, accessibility support, English user-facing text, and `en-US` number/date formatting.
7. Do not add features outside the requested scope, and do not change the backend schema unless necessary to fix a defect.

Working rules:
- Inspect the actual workspace files; do not rely on code pasted into the prompt.
- When you find a defect, edit the affected file directly and keep the existing backend schema unless a schema change is necessary to fix the defect.
- Re-open the files after editing and run every available local syntax, static, or automated check that does not require a live Google account.
- Do not print either complete file in the chat response.

Your final response must include:
- A list of findings grouped by Critical, Major, and Minor severity. For each finding, identify the file, location or function, impact, and recommended fix.
- A concise list of changes made directly to `Code.gs` or `Index.html`.
- A final PASS/FAIL checklist for items 1 through 7.
- Any behavior that still needs to be verified in the Apps Script Editor or deployed Web App.

Do not assume the code is correct merely because it was generated by AI. If you are uncertain about a Google Apps Script API, explicitly state what must be verified in the Apps Script Editor.

Hình 3. Codex hoàn tất review hai file với checklist 1–7 đều PASS. Kết quả cũng nêu rõ các nội dung vẫn phải kiểm tra trên môi trường thật, gồm quyền truy cập Google Sheets, timezone, google.script.run và khả năng render Google Charts.

Luồng thực hành vì vậy không phải “prompt một lần rồi sử dụng ngay”, mà là:

Viết yêu cầu → Codex tạo Code.gs → Codex tạo Index.html
→ Codex review và sửa file → chạy kiểm tra tự động → chép file vào GAS
→ deploy Web App → kiểm tra thủ công

Kết quả sinh code và review

Sau khi nhập lần lượt ba prompt trên, workspace có hai file chính: Code.gs cho phần đọc, lọc và tổng hợp dữ liệu; Index.html cho giao diện dashboard. Kết quả có đủ bốn KPI, ba biểu đồ, bảng ticket ưu tiên, bộ lọc và các trạng thái đang tải, có lỗi, không có dữ liệu.

Tôi kiểm tra lại cú pháp, công thức KPI, tên trường được trao đổi giữa hai file và cách giao diện xử lý dữ liệu. Dashboard cũng được mở trên trình duyệt với dữ liệu mẫu để kiểm tra bố cục. Kết quả định lượng của lần chạy được trình bày ở mục 7 sau khi hoàn tất các bước triển khai.

Cùng một prompt có thể tạo ra output khác nhau giữa các model hoặc giữa hai lần chạy. Vì vậy, prompt-to-code giúp rút ngắn thời gian tạo bản đầu tiên nhưng không thay thế bước review, kiểm tra tự động và chạy thật trong Apps Script Editor.

Mã nguồn, dữ liệu mẫu và hướng dẫn thực hành được đăng trên GitHub để người đọc có thể kiểm tra hoặc tự chạy lại:

Mã nguồn GitHub: mhieupham1/GAS-HTML-Service

Sau khi có hai file do Codex tạo, tôi đưa chúng vào Google Apps Script và chạy thử theo các bước dưới đây.

6. Thực hành trên Google Apps Script

Bước 1: Chuẩn bị Google Sheet

Đầu tiên, tôi tạo một Google Sheet mới rồi import file sample-data.csv. Sheet chứa dữ liệu phải được đặt tên chính xác là Tickets, vì đây là tên mà backend sẽ tìm khi chạy.

Sau khi import, tôi kiểm tra lại hàng tiêu đề, số lượng 40 dòng dữ liệu và định dạng ngày. Mỗi dòng cần có đủ mười trường sau:

ticket_id, created_at, service, category, priority,
status, assignee_team, resolved_at, resolution_hours, sla_met

Các cột resolved_at, resolution_hours và sla_met được để trống đối với ticket chưa xử lý xong. Với ticket có trạng thái Resolved, ba cột này phải có dữ liệu hợp lệ để backend tính thời gian xử lý và tỷ lệ SLA.

Hình 4. Dữ liệu ticket mẫu sau khi được import vào Google Sheets và đặt tên sheet là Tickets.

Bước 2: Sao chép file backend vào Code.gs của Apps Script

File trong workspace của Codex và file trong Google Apps Script là hai file ở hai môi trường khác nhau. Vì vậy, sau khi Codex tạo code xong, tôi cần chuyển nội dung file sang Apps Script Editor.

Từ Google Sheet, tôi mở Extensions → Apps Script để tạo một bound script. Apps Script tạo sẵn file Code.gs với hàm myFunction(). Trong workspace của Codex, tôi mở file Code.gs, chọn toàn bộ nội dung bằng Ctrl+A hoặc Cmd+A, rồi sao chép bằng Ctrl+C hoặc Cmd+C. Tôi quay lại Apps Script Editor, xóa hàm mặc định và dán nội dung vào file Code.gs như Hình 5.

Hình 5. Nội dung Code.gs phiên bản tiếng Anh vừa được dán vào Apps Script Editor, trước khi lưu thay đổi.

Sau khi kiểm tra lại nội dung, tôi bấm Save để lưu project.

Trong Code.gs, hàm doGet() là điểm bắt đầu khi người dùng mở URL của Web App. Hàm này yêu cầu GAS trả về giao diện nằm trong file Index.html:

function doGet() {
  return HtmlService.createHtmlOutputFromFile('Index')
    .setTitle('IT Ticket & SLA Dashboard')
    .addMetaTag('viewport', 'width=device-width, initial-scale=1');
}

Khi giao diện cần dữ liệu, hàm getDashboardData() đọc sheet, chuẩn hóa các dòng và tạo dữ liệu tổng hợp để gửi lại trình duyệt:

function getDashboardData(filters) {
  var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
  if (!spreadsheet) {
    throw new Error('No active spreadsheet was found. Bind this Apps Script project to the Google Sheet containing the data.');
  }

  var sheet = spreadsheet.getSheetByName(TICKETS_SHEET_NAME);

  if (!sheet) {
    throw new Error('The "Tickets" sheet was not found.');
  }

  var values = sheet.getDataRange().getValues();
  if (values.length === 0 || values[0].length === 0) {
    throw new Error('The "Tickets" sheet does not have a header row.');
  }

  var headerMap = buildHeaderMap_(values[0]);
  assertRequiredHeaders_(headerMap);

  var timeZone = spreadsheet.getSpreadsheetTimeZone() || Session.getScriptTimeZone();
  var normalizedFilters = normalizeFilters_(filters, timeZone);
  var parsed = parseTicketRows_(values.slice(1), headerMap, timeZone);
  var allTickets = parsed.tickets;
  var filteredTickets = filterTickets_(allTickets, normalizedFilters);
  var dataBounds = getDateBounds_(allTickets);

  return {
    kpis: buildKpis_(filteredTickets),
    trends: buildTrends_(filteredTickets),
    byService: buildCountSeries_(filteredTickets, 'service'),
    byStatus: buildStatusSeries_(filteredTickets),
    urgentTickets: buildUrgentTickets_(filteredTickets),
    options: buildOptions_(allTickets),
    meta: {
      periodStart: normalizedFilters.startDate || (normalizedFilters.endDate ? null : dataBounds.start),
      periodEnd: normalizedFilters.endDate || (normalizedFilters.startDate ? null : dataBounds.end),
      filteredCount: filteredTickets.length,
      skippedRows: parsed.skippedRows,
      generatedAt: new Date().toISOString()
    }
  };
}

Bước 3: Tạo Index.html và dán code giao diện

Trong Apps Script Editor, tôi bấm dấu + cạnh mục Files, chọn HTML, nhập tên Index rồi xác nhận. Apps Script tự thêm phần mở rộng, vì vậy file xuất hiện với tên Index.html.

Tôi xóa nội dung HTML mặc định. Trong workspace của Codex, tôi mở file Index.html, chọn toàn bộ nội dung và sao chép, sau đó dán vào file Index.html vừa tạo trong Apps Script Editor. Tên Index phải khớp với câu lệnh HtmlService.createHtmlOutputFromFile(‘Index’) trong Code.gs.

Hình 6. File Index.html được tạo trong Apps Script Editor và đã nhận toàn bộ nội dung HTML, CSS và JavaScript do Codex sinh ra.

Phía trình duyệt dùng hàm getFilters() để thu thập bốn giá trị lọc:

function getFilters() {
  return {
    startDate: elements.startDate.value,
    endDate: elements.endDate.value,
    service: elements.service.value,
    priority: elements.priority.value
  };
}

Trong loadDashboard(), object này được lưu vào biến filters rồi gửi tới backend bằng google.script.run:

google.script.run
  .withSuccessHandler(handleSuccess)
  .withFailureHandler(handleFailure)
  .getDashboardData(filters);

Sau khi nhận kết quả, client cập nhật KPI, ba biểu đồ và bảng ưu tiên. Dữ liệu ticket được gắn vào DOM bằng textContent thay vì ghép trực tiếp vào innerHTML. Giao diện có bốn trạng thái: đang tải, có lỗi, không có dữ liệu và hiển thị dashboard. Nút lọc bị khóa trong lúc tải để tránh gửi nhiều yêu cầu cùng lúc.

Bước 4: Chạy thử backend

Trên thanh công cụ của Apps Script Editor, tôi chọn hàm getDashboardData trong danh sách hàm rồi bấm Run. Ở lần chạy đầu tiên, Google yêu cầu cấp quyền đọc bảng tính. Tôi chọn tài khoản đang sở hữu file và hoàn tất màn hình cấp quyền.

Khi khu vực Execution log hiển thị trạng thái Execution completed, backend đã đọc và xử lý được sheet Tickets mà không phát sinh lỗi. Bước này chưa mở giao diện dashboard; nó chỉ xác nhận phần server có thể chạy.

Hình 7. Hàm getDashboardData được chạy trong Apps Script Editor và hoàn tất với trạng thái Execution completed.

Nếu xuất hiện lỗi thiếu sheet hoặc thiếu header, tôi quay lại Google Sheets để kiểm tra tên Tickets và mười tên cột trước khi tiếp tục.

Bước 5: Triển khai Web App

Sau khi backend chạy thành công, tôi chọn Deploy → New deployment, chọn loại Web app rồi cấu hình deployment. Với demo cá nhân, lựa chọn an toàn ban đầu là thực thi dưới tài khoản của chủ project và giới hạn quyền truy cập ở Only myself.

Hình 8. Cấu hình một deployment mới với loại Web app, chạy bằng tài khoản chủ project và giới hạn quyền truy cập ở Only myself.

Khi sử dụng dữ liệu thật của doanh nghiệp, không nên chọn truy cập công khai nếu chưa đánh giá dữ liệu, quyền riêng tư và chính sách của tổ chức. Mỗi lần sửa code, cần tạo version mới trong phần quản lý deployment để Web App sử dụng phiên bản cập nhật.

Sau khi bấm Deploy, Apps Script cung cấp một Web App URL. Tôi mở URL này trong tab mới và chờ giao diện tải dữ liệu. Dashboard chạy thành công khi bốn KPI, ba biểu đồ và bảng ticket ưu tiên đều xuất hiện mà không có thông báo lỗi.

Hình 9. Dashboard sau khi triển khai thành công, gồm bộ lọc, bốn KPI và biểu đồ xu hướng số ticket theo ngày.

Bước 6: Thử bộ lọc và các trạng thái

Dashboard cho phép lọc theo khoảng ngày, service và priority. Trong lần thử này, tôi chỉ nhập ngày bắt đầu 10/09/2026 và ngày kết thúc 13/09/2026, đồng thời giữ nguyên All services và All priorities. Sau khi bấm Apply, dashboard còn 10 ticket trong khoảng ngày đã chọn. Các KPI được tính lại thành 4 ticket chưa đóng, tỷ lệ đạt SLA 83,3% và thời gian xử lý trung bình 9,5 giờ. Biểu đồ xu hướng cũng chỉ còn dữ liệu của bốn ngày từ 10/09 đến 13/09.

Tôi cũng kiểm tra các trường hợp biên trong thiết kế: khoảng ngày không có ticket sẽ hiển thị empty state; ngày bắt đầu sau ngày kết thúc sẽ hiển thị lỗi; khi thu hẹp trình duyệt, các KPI và biểu đồ chuyển thành một cột.

Hình 10. Kết quả sau khi lọc dữ liệu từ ngày 10/09/2026 đến 13/09/2026 và giữ nguyên tất cả service, priority.

Sau khi dashboard chạy được và các trường hợp lọc đã được kiểm tra, tôi đối chiếu output với dữ liệu gốc và các nguyên tắc thiết kế đã chọn.

7. Kết quả và đối chiếu với guidebook

Với toàn bộ 40 ticket, backend tạo ra kết quả sau:

Chỉ số Kết quả
Tổng ticket 40
Ticket chưa đóng 16
Tỷ lệ đạt SLA 79,2%
Thời gian xử lý trung bình 12,8 giờ
Service có nhiều ticket nhất Payment API — 14
Ticket High/Critical chưa đóng 10

Ở phiên bản mã nguồn dùng cho bài viết, tôi chạy bộ kiểm tra cục bộ bằng Node.js và nhận kết quả 20 test PASS, 0 test FAIL. Các trường hợp được kiểm tra gồm công thức KPI, dữ liệu mẫu 40 ticket, bộ lọc ngày inclusive, kết quả rỗng, dòng dữ liệu không hợp lệ, chuẩn hóa ngày từ Google Sheets, thông báo lỗi, hợp đồng dữ liệu frontend–backend, bốn trạng thái giao diện và việc render ba biểu đồ.

npm test

Tôi tiếp tục xác nhận trên môi trường thật bằng cách chạy getDashboardData trong Apps Script Editor và nhận trạng thái Execution completed như Hình 7. Trên Web App, toàn bộ dữ liệu tạo ra bốn KPI 40 / 16 / 79,2% / 12,8 giờ như Hình 9. Khi chỉ lọc khoảng ngày từ 10/09/2026 đến 13/09/2026, kết quả được tính lại thành 10 / 4 / 83,3% / 9,5 giờ như Hình 10. Các kết quả này được đối chiếu với dữ liệu trong sheet Tickets.

Bộ kiểm tra tự động và kiểm tra chức năng trên bản demo chưa thay thế cho kiểm thử đầy đủ về tải, bảo mật và accessibility trước khi sử dụng trong môi trường production.

Đối chiếu với guidebook, dashboard đã áp dụng các nguyên tắc theo cách sau:

Nguyên tắc Cách áp dụng trong demo
Bắt đầu từ mục tiêu Xác định bốn câu hỏi nghiệp vụ trước khi chọn biểu đồ
Tổng quan đến chi tiết KPI → biểu đồ → bảng ticket ưu tiên
Chọn biểu đồ đúng mục đích Line cho thời gian, bar/column cho so sánh
Không làm sai lệch Trục bắt đầu từ 0, không dùng 3D
Sắp xếp có ý nghĩa Service giảm dần; Critical trước High
Metadata rõ ràng Hiển thị kỳ, nguồn và thời điểm tạo báo cáo
Không dựa riêng vào màu Badge có cả màu và chữ
Phản hồi trạng thái Có loading, error và empty state

Kết quả trên cho thấy demo đáp ứng mục tiêu thực hành, nhưng chưa có nghĩa là giải pháp đã sẵn sàng cho môi trường production.

8. Giới hạn của demo

Demo sử dụng dữ liệu giả lập và chưa đồng bộ tự động với Jira, GitHub Issues hoặc hệ thống quản lý incident.

GAS và Google Sheets cũng có giới hạn về thời gian thực thi, kích thước dữ liệu và lượng truy cập. Google Charts phụ thuộc vào kết nối mạng phía trình duyệt. Khi số bản ghi hoặc số người dùng tăng, cần cân nhắc sử dụng data warehouse và nền tảng BI chuyên dụng. Trước khi đưa vào production, dashboard cũng cần được kiểm tra sâu hơn về quyền truy cập, dữ liệu cá nhân, accessibility và quy trình cập nhật dữ liệu.

Những giới hạn này không làm mất giá trị của prototype; chúng giúp xác định rõ tình huống nào phù hợp với GAS và khi nào cần chuyển sang một giải pháp lớn hơn.

9. Quan điểm cá nhân và ứng dụng vào công việc hằng ngày

Theo tôi, giá trị lớn nhất của phương pháp này không nằm ở việc tạo được nhiều biểu đồ, mà ở việc biến dữ liệu thành tín hiệu hành động. Trong daily stand-up, thay vì đọc toàn bộ danh sách ticket, đội có thể bắt đầu từ số ticket chưa đóng và bảng Critical/High. Điều này giúp cuộc họp tập trung vào vấn đề cần quyết định thay vì mất thời gian tổng hợp dữ liệu.

Trong bug triage, biểu đồ theo service giúp nhận biết khu vực đang phát sinh nhiều lỗi để ưu tiên root-cause analysis hoặc bổ sung test. Biểu đồ xu hướng theo ngày có thể được dùng để quan sát thay đổi sau một đợt release. Trong retrospective, tỷ lệ SLA và thời gian xử lý trung bình cung cấp cơ sở để đánh giá liệu cải tiến quy trình có tạo ra kết quả hay không.

Quy trình làm rõ yêu cầu, tạo prototype và lấy phản hồi cũng có thể áp dụng cho các công cụ nội bộ khác. Trước khi code, tôi nên ghi lại người dùng, câu hỏi, quyết định và định nghĩa dữ liệu. Một prototype với dữ liệu giả là đủ để kiểm tra bố cục và đạt đồng thuận với stakeholder. Việc này giảm nguy cơ xây xong mới phát hiện các bên hiểu KPI theo những cách khác nhau.

GAS HTML Service đặc biệt phù hợp với những workflow nhỏ đang sử dụng Google Workspace, chẳng hạn theo dõi pull request, incident, tài sản IT, chi phí cloud hoặc tiến độ đào tạo. Tôi có thể bắt đầu bằng Sheets và GAS để kiểm chứng giá trị với chi phí thấp. Nếu dashboard được sử dụng thường xuyên và nhu cầu dữ liệu tăng, lúc đó mới đầu tư kết nối Jira/GitHub tự động hoặc chuyển sang nền tảng BI lớn hơn.

Một cạm bẫy cần tránh là biến dashboard thành bộ sưu tập biểu đồ. Mỗi khi muốn thêm một chart, tôi nên hỏi ai sẽ sử dụng nó và họ sẽ làm gì khác đi sau khi xem. Nếu không có câu trả lời rõ ràng, biểu đồ đó có thể không cần xuất hiện. Dashboard cũng nên được xem như một sản phẩm cần theo dõi mức sử dụng, lấy phản hồi và cập nhật khi quy trình hoặc định nghĩa dữ liệu thay đổi.

10. Kết luận

GAS HTML Service là một cách nhẹ để biến Google Sheets thành dashboard Web App mà không cần vận hành nhiều hạ tầng. Tuy nhiên, công nghệ chỉ giải quyết phần triển khai. Guidebook của Digital Agency bổ sung phần quan trọng hơn: bắt đầu từ mục tiêu, prototype để đạt đồng thuận, tổ chức thông tin theo thứ bậc, chọn biểu đồ đúng câu hỏi, tránh làm sai lệch dữ liệu và quan tâm đến khả năng tiếp cận.

Qua demo IT Ticket & SLA, tôi nhận thấy một dashboard nhỏ vẫn có thể mang lại giá trị nếu nó trả lời đúng câu hỏi và dẫn tới hành động cụ thể. Bước tiếp theo của mẫu này có thể là lấy dữ liệu tự động từ Jira hoặc GitHub Issues, tính SLA theo chính sách thực tế và đánh giá giao diện với các thành viên trong đội IT.

Nguồn tham khảo

Tags: