Skip to content

Dev Spec: Cải thiện UX cài đặt Mã Công Việc / Tiền Tour

Feature slug: settings-ux-subtask-tagVersion: 1.0 Ngày: 2026-03-26


C1) Scope

LoạiNội dung
In scopeTrang TourFeeConfig (gộp), TaskTagSelect enriched, SubTaskTable cải thiện, blast radius display, rename, route redirects
Out of scopeInline-create tag, bulk operations, backend changes, DB changes, Hasura metadata changes

C2) Impact

LayerFileThay đổi
FE (new)settings/pages/TourFeeConfig.tsxTrang mới — accordion layout
FE (new)settings/components/tour-fee/TourFeeGroupAccordion.tsxComponent nhóm collapsible
FE (new)settings/components/tour-fee/TourFeeTagTable.tsxBảng tags + range + chi tiết
FE (new)settings/components/tour-fee/TourFeeUsageExpand.tsx"Đang dùng bởi" expandable
FE (sửa)settings/components/task-tag/TaskTagSelect.tsxEnriched chip + dropdown + tooltip
FE (sửa)settings/components/task-tag/TaskTagDisplay.tsxDisplay format: name → code (range)
FE (sửa)settings/components/subtask/SubTaskForm.tsxRename label
FE (sửa)settings/components/subtask/SubTaskTable.tsxCột tag + filter + fix label swap
FE (sửa)settings/pages/SubTask.tsxRename title
FE (sửa)settings/module.tsRoute mới + redirects + sidebar rename
FE (sửa)settings/types.tsRoute constants
FE (blast)ecommerce/components/order/OrderForm/OrderTaskLimitForm.tsxTag display format
FE (blast)ecommerce/components/service/ServiceForm/ServiceSubtaskItem.tsxTag display format
GraphQLsettings/graphql/task-tag.graphqlFragment thêm tag_tour_moneys
GraphQLsettings/graphql/subtask.graphqlFragment thêm tag.code + tag_tour_moneys

C3) Rules & Formulas

FORMULA-001: Tour Fee Range

  • Mô tả: Tính range tiền tour từ bảng tour_moneys của 1 tag.
  • Công thức:
    • min = MIN(tag_tour_moneys.tour_money)
    • max = MAX(tag_tour_moneys.tour_money)
    • Nếu min === max → flat-rate: "${formatCurrency(min)}đ"
    • Nếu min !== max → multi-rate: "${formatShort(min)}~${formatShort(max)}"
    • Nếu tag_tour_moneys.length === 0"—"
  • Biến số:
    • tour_money: số tiền tour — nguồn: tag_tour_moneys.tour_money
  • Đơn vị: VNĐ
  • Ví dụ:
    • T26: [100k, 100k, 100k, 100k, 100k] → flat → "100,000đ"
    • T15: [50k, 60k, 80k, 100k, 120k] → multi → "50k~120k"
    • T99: [] → empty → "—"
  • Edge cases:
    • Tất cả = 0 → flat → "0đ"
    • Chỉ 1 cấp bậc → flat → "{amount}đ"
    • NULL tour_money → exclude khỏi min/max

FORMULA-002: Tour Fee Detail Summary

  • Mô tả: Tóm tắt chi tiết tiền tour theo cấp bậc.
  • Công thức:
    • Flat-rate: "CĐ1-{n}: đều {amount}"
    • Multi-rate: "CĐ1:{amount1} CĐ2:{amount2} ... CĐn:{amountn}"
    • Empty: "Chưa có tiền tour"
  • Ví dụ:
    • T26: "CĐ1-5: đều 100k"
    • T15: "CĐ1:50k CĐ2:60k CĐ3:80k CĐ4:100k CĐ5:120k"

C4) Data Model

ZERO database changes. Tất cả tables đã tồn tại:

TableDBVai trò trong feature
project_task_group_tagprojectNhóm tiền tour (accordion header)
project_task_tagprojectTag tiền tour (accordion content)
tag_tour_moneyprojectTiền tour theo cấp bậc (range + chi tiết)
subtaskecommerceMã công việc (SubTaskTable)
subtask_tagecommerceJunction subtask ↔ tag (cột tag + "đang dùng bởi")

C5) API & GraphQL

Fragment mở rộng — task-tag.graphql

Thêm vào fragment TaskTag:

graphql
fragment TaskTag on project_task_tag {
  code
  group_tag_id
  project_task_group_tag {
    name
  }
  tag_tour_moneys {        # MỚI
    seniority
    tour_money
    level_info {           # MỚI — cho tooltip
      name
    }
  }
  id
  name
  disabled
}

Fragment mở rộng — subtask.graphql

Thêm vào fragment Subtask:

graphql
fragment Subtask on subtask {
  subtask_tags {
    tag_id
    tag {
      name
      code               # MỚI
      tag_tour_moneys {   # MỚI
        tour_money
      }
    }
  }
  code
  default_subtask
  description
  disabled
  id
  name
  type
}

Query: Trang TourFeeConfig

Reuse GetTaskTagGroup — mở rộng nested data:

graphql
query GetTaskTagGroupWithTags($where: project_task_group_tag_bool_exp, $order_by: [project_task_group_tag_order_by!]) {
  project_task_group_tag(where: $where, order_by: $order_by) {
    id
    code
    name
    disabled
    project_task_tags(order_by: { code: asc }) {
      id
      code
      name
      disabled
      tag_tour_moneys {
        seniority
        tour_money
        level_info {
          name
        }
      }
    }
    project_task_tags_aggregate {
      aggregate {
        count
      }
    }
  }
}

Query: "Đang dùng bởi" (lazy load)

graphql
query GetSubtasksByTagIds($tagIds: [uuid!]!) {
  subtask(
    where: { subtask_tags: { tag_id: { _in: $tagIds } } }
    order_by: { code: desc }
    limit: 50
  ) {
    id
    code
    name
    subtask_tags {
      tag_id
      tag {
        code
        name
      }
    }
  }
}

Error Contract

ScenarioErrorFE handling
GetTaskTagGroupWithTags failGraphQL errorToast "Không thể tải dữ liệu tiền tour"
GetSubtasksByTagIds failGraphQL errorToast "Không thể tải danh sách mã công việc"
Xóa nhóm có tagsValidation block (FE side)Toast "Nhóm đang có {N} tag. Xóa hoặc chuyển tag trước."

C6) Scheduler

Không có scheduler.


C7) Migration

ZERO migration. Không thay đổi database.


C8) Security

ConcernMitigation
Unauthorized access TourFeeConfigGiữ nguyên route guard: UserRole.ITLeader, UserRole.ITStaff
Route redirect bypassRedirect routes cũng require same permissions
Cross-module data exposureTag data đã public cho role user (Hasura permission đã có)

C9) NFR

IDRequirementMetricTarget
NFR-001TourFeeConfig page loadFirst meaningful paint< 1s (lazy load accordion)
NFR-002Accordion expandTags render time< 300ms per group
NFR-003SubTaskTable with tag columnLoad 275+ rows< 500ms
NFR-004TaskTagSelect dropdownOptions load< 300ms (existing limit 10)
NFR-005"Đang dùng bởi" expandLazy query time< 500ms

C10) Observability

MetricCách đoAlert threshold
TourFeeConfig page load timePerformance APIp99 > 3s → investigate
GetTaskTagGroupWithTags query timeHasura request durationp99 > 1s → alert
GetSubtasksByTagIds query timeHasura request durationp99 > 1s → alert
Route redirect miss (404)Client error logAny 404 on old routes → alert

C11) Tasks (Dev breakdown)

#TaskEstimateDependency
T1Mở rộng GraphQL fragments (task-tag + subtask) + pnpm codegen0.5d
T2Utility function computeTourFeeRange() + computeTourFeeDetail()0.25dT1
T3TaskTagSelect enriched: chip format + dropdown option format1dT1, T2
T4TaskTagSelect tooltip: hover chip → bảng tiền tour + link0.5dT3
T5TourFeeConfig page + route + sidebar0.5dT1
T6TourFeeGroupAccordion (expand/collapse, search, add group)1.5dT5
T7TourFeeTagTable (bảng tags + range + chi tiết + status + actions)1dT2, T6
T8TourFeeUsageExpand (lazy query + expand/collapse)0.5dT7
T9Reuse TaskTagForm trong TourFeeConfig (side dialog)0.5dT6
T10SubTaskTable: thêm cột "Tag tiền tour"0.5dT1, T2
T11SubTaskTable: thêm filter nhóm/tag + fix label swap0.5dT10
T12SubTaskForm: rename labels0.1d
T13OrderTaskLimitForm: sửa tag display format0.25dT2
T14ServiceSubtaskItem: sửa tag display format0.25dT2
T15TaskTagDisplay: enriched format0.25dT2
T16Route redirects + route name mapping0.25dT5
T17Sidebar rename (module.ts)0.1dT5
T18Testing + fix + responsive1.5dAll
Tổng~10.85d

C12) Traceability

FR/NFRFE ArtifactTC-IDStatus
FR-001 Trang TourFeeConfigSCR-01: TourFeeConfig + Accordion + TagTableTC-FR-001Planned
FR-002 Range + Chi tiếtSCR-01: TourFeeTagTable columnsTC-FR-002Planned
FR-003 "Đang dùng bởi"SCR-01: TourFeeUsageExpandTC-FR-003Planned
FR-004 Tag chip enrichedSCR-03,04,05,06: TaskTagSelect + DisplayTC-FR-004Planned
FR-005 Tooltip tiền tourSCR-03: TaskTagSelect tooltipTC-FR-005Planned
FR-006 Dropdown enrichedSCR-03,04: TaskTagSelect dropdownTC-FR-006Planned
FR-007 SubTaskTable cải thiệnSCR-02: SubTaskTableTC-FR-007Planned
FR-008 Rename + redirectsSidebar + routesTC-FR-008Planned
NFR-001 Page loadPerformance testTC-NFR-001Planned
NFR-003 SubTaskTable loadPerformance testTC-NFR-002Planned