---
date: 2026-05-28
type: improve-plan
epic_id: EPIC-006
repo: web-kingston
title: EPIC-006 Web — Full Fix Plan (dựa trên code audit thực tế)
---

# IMPROVE-PLAN — EPIC-006 (web-kingston)

> **Base path:** `packages/main-app/src/clean-architecture/`
> **Rule ref:** `.claude/skills/generate-*` trong web-kingston
> Layers domain/application/infrastructure đã đúng sau các fix trước. Chỉ sửa presentation + useCase naming.

---

## Nhóm A — UseCase naming (no `UseCase` suffix)

**Rule:** `generate-usecase/SKILL.md` — "Class name: PascalCase, không có suffix UseCase"

### A1. Rename 11 useCase files

| File hiện tại | File đúng | Class hiện tại | Class đúng |
|---|---|---|---|
| `useCases/variant/GetVariantUseCase.ts` | `GetVariantById.ts` | `GetVariantUseCase` | `GetVariantById` |
| `useCases/variant/GetVariantsUseCase.ts` | `GetVariants.ts` | `GetVariantsUseCase` | `GetVariants` |
| `useCases/variant/GetVariantsByStructureUseCase.ts` | `GetVariantsByStructureId.ts` | `GetVariantsByStructureUseCase` | `GetVariantsByStructureId` |
| `useCases/variant/CreateVariantUseCase.ts` | `CreateVariant.ts` | `CreateVariantUseCase` | `CreateVariant` |
| `useCases/variant/UpdateVariantUseCase.ts` | `UpdateVariant.ts` | `UpdateVariantUseCase` | `UpdateVariant` |
| `useCases/variant/DeleteVariantUseCase.ts` | `DeleteVariant.ts` | `DeleteVariantUseCase` | `DeleteVariant` |
| `useCases/componentType/GetComponentTypesUseCase.ts` | `GetComponentTypes.ts` | `GetComponentTypesUseCase` | `GetComponentTypes` |
| `useCases/componentType/GetAttributeTypesUseCase.ts` | `GetAttributeTypes.ts` | `GetAttributeTypesUseCase` | `GetAttributeTypes` |
| `useCases/componentType/CreateComponentTypeUseCase.ts` | `CreateComponentType.ts` | `CreateComponentTypeUseCase` | `CreateComponentType` |
| `useCases/componentType/UpdateComponentTypeUseCase.ts` | `UpdateComponentType.ts` | `UpdateComponentTypeUseCase` | `UpdateComponentType` |
| `useCases/componentType/DeleteComponentTypeUseCase.ts` | `DeleteComponentType.ts` | `DeleteComponentTypeUseCase` | `DeleteComponentType` |

### A2. Cập nhật DI container sau khi rename

File `di/container.ts` — đổi tất cả import class names từ `*UseCase` sang không suffix.
File `di/types.ts` — các Symbol key giữ nguyên (e.g. `GetVariants`, `CreateVariant`).

### A3. Cập nhật hooks import sau khi rename

Mỗi hook hiện đang import useCase class — cần đổi import path + class name:

```typescript
// Trước
import { GetVariantsUseCase } from "clean-architecture/application/useCases/variant/GetVariantsUseCase";
const useCase = container.get<GetVariantsUseCase>(TYPES.GetVariants);

// Sau
import { GetVariants } from "clean-architecture/application/useCases/variant/GetVariants";
const useCase = container.get<GetVariants>(TYPES.GetVariants);
```

Các hooks bị ảnh hưởng:
- `useGetVariants.ts`, `useGetVariant.ts`, `useGetVariantsByStructure.ts`
- `useCreateVariant.ts`, `useUpdateVariant.ts`, `useDeleteVariant.ts`
- `useGetComponentTypes.ts`, `useGetAttributeTypes.ts`
- `useCreateComponentType.ts`, `useUpdateComponentType.ts`, `useDeleteComponentType.ts`

---

## Nhóm B — Hooks fixes

### B1. Rename `useGetVariant.ts` → `useGetVariantById.ts`

**Rule:** `generate-presentation-hooks` — "File tên: `useGet<Entity>ById.tsx`"

Đổi:
- Filename: `useGetVariant.ts` → `useGetVariantById.ts`
- Export name: `useGetVariant` → `useGetVariantById`
- `VARIANT_QUERY_KEY` giữ nguyên (đang dùng ở `useVariantActions.ts`)
- Cập nhật barrel `hooks/index.ts`
- Cập nhật import trong `useVariantActions.ts`, `VariantDetailPage.tsx`, `VariantFormPage.tsx`

### B2. Fix `useGetComponentTypes.ts` — return PaginationResult thay vì unwrap

**Hiện tại:**
```typescript
return {
  componentTypes: data?.result ?? [],  // ❌ đã unwrap
  totalComponentTypes: data?.total ?? 0,
  ...
};
```

**Fix:**
```typescript
return {
  componentTypes: data,  // ✅ PaginationResult<ComponentType> | undefined
  isComponentTypesLoading: isLoading,
  ...
};
```

> `ComponentTypeListPage` truyền `data={componentTypes}` hiện đang dùng array (do hook unwrap). Sau fix phải sửa page luôn.

### B3. Fix `useGetVariantColumns.tsx` — ViewModel + `useAppTranslation` + no raw button

**Violations:**
- `ColumnDef<Variant>[]` → `ColumnDef<VariantView>[]`
- `onAction?: (action, item: Variant)` → `(action, item: VariantView)`
- Hardcoded "Tên Variant", "Sản phẩm", "Kết cấu", "Ngày tạo", "Sửa", "Xóa"
- Raw `<button className="text-blue-500...">` → dùng action component từ existing patterns

**Pattern đúng** (xem `useGetOrderColumns`, `useGetBomColumns`):
```typescript
import { VariantView } from "clean-architecture/presentation/viewModels/VariantView";

interface UseGetVariantColumnsProps {
  handleActionClick: (action: VariantAction, data: VariantView | null) => void;
}

export const useGetVariantColumns = ({ handleActionClick }: UseGetVariantColumnsProps) => {
  const { variantText } = useAppTranslation();
  const columns: ColumnDef<VariantView>[] = useMemo(() => [
    { header: variantText.field.name, accessorKey: "name" },
    { header: variantText.field.product, accessorKey: "productName" },
    // ...action column dùng component từ existing patterns
  ], [variantText, handleActionClick]);
  return { columns };
};
```

### B4. Fix `useGetComponentTypeColumns.tsx` — ViewModel + `useAppTranslation` + no raw button

Tương tự B3 cho ComponentType.

### B5. Cập nhật barrel `hooks/index.ts` (variant)

Sau khi rename `useGetVariant` → `useGetVariantById`:
```typescript
export * from "./useGetVariants";
export * from "./useGetVariantById";   // ← đổi tên
export * from "./useGetVariantsByStructure";
export * from "./useCreateVariant";
export * from "./useUpdateVariant";
export * from "./useDeleteVariant";
export * from "./useGetVariantColumns";
export * from "./useVariantActions";
```

---

## Nhóm C — Tạo files thiếu

### C1. `viewModels/VariantView.tsx`

```typescript
import { Variant } from "clean-architecture/domain/entities/Variant";
import { formatDate } from "clean-architecture/presentation/helpers/utils";

export class VariantView {
  constructor(public readonly variant: Variant) {}

  private default(value?: any, fallback = "-") { return value ?? fallback; }

  get id(): ID { return this.default(this.variant.id); }
  get name(): string { return this.default(this.variant.name); }
  get productName(): string { return this.default(this.variant.product?.nameVn); }
  get productStructureName(): string { return this.default(this.variant.productStructure?.name); }
  get createdAt(): string { return this.default(this.variant.createdAt); }

  renderCreatedAt(): string {
    return formatDate(this.variant.createdAt);
  }
}
```

### C2. `viewModels/ComponentTypeView.tsx`

```typescript
import { ComponentType } from "clean-architecture/domain/entities/ComponentType";

export class ComponentTypeView {
  constructor(public readonly componentType: ComponentType) {}

  private default(value?: any, fallback = "-") { return value ?? fallback; }

  get id(): ID { return this.default(this.componentType.id); }
  get name(): string { return this.default(this.componentType.name); }
  get attributeCount(): number { return this.componentType.attributeTypes?.length ?? 0; }
}
```

### C3. i18n keys

**File:** `locales/vn.json` và `locales/en.json`

Thêm keys cho `variantText` và `componentTypeText`:
```json
{
  "variant": {
    "title": "Variant",
    "add": "Tạo Variant mới",
    "edit": "Chỉnh sửa Variant",
    "detail": {
      "title": "Chi tiết Variant",
      "tab": { "general": "Thông tin chung" },
      "information": "Thông tin cơ bản",
      "attributes": "Thông số sản phẩm"
    },
    "field": {
      "name": "Tên Variant",
      "product": "Sản phẩm",
      "productStructure": "Kết cấu",
      "createdAt": "Ngày tạo"
    },
    "error": {
      "nameRequired": "Vui lòng nhập tên Variant",
      "productRequired": "Vui lòng chọn sản phẩm",
      "productStructureRequired": "Vui lòng chọn kết cấu"
    }
  },
  "componentType": {
    "title": "Loại component",
    "add": "Tạo loại mới",
    "edit": "Sửa loại component",
    "field": {
      "name": "Tên loại component",
      "attributeCount": "Số thuộc tính"
    },
    "error": {
      "nameRequired": "Vui lòng nhập tên loại"
    }
  }
}
```

### C4. `presentation/forms/variant/VariantFormValues.ts`

```typescript
import { OptionSelect } from "clean-architecture/shared";

export interface VariantStep1Values {
  name: string;
  product: OptionSelect<ID> | null;
  productStructure: OptionSelect<ID> | null;
}

export interface VariantAttributeValue {
  componentId: ID;
  attributeTypeId: ID;
  value: string;
}
```

### C5. `presentation/modules/variant/mappers.ts`

```typescript
import { Variant } from "clean-architecture/domain/entities/Variant";
import { CreateVariantArgs, UpdateVariantArgs } from "clean-architecture/domain/repositories/VariantRepository";
import { VariantStep1Values, VariantAttributeValue } from "clean-architecture/presentation/forms/variant/VariantFormValues";

export class VariantFormMapper {
  static step1ToForm(variant?: Variant): VariantStep1Values {
    return {
      name: variant?.name ?? "",
      product: variant?.product
        ? { label: variant.product.nameVn, value: variant.product.id }
        : null,
      productStructure: variant?.productStructure
        ? { label: variant.productStructure.name, value: variant.productStructure.id }
        : null,
    };
  }

  static toCreateArgs(step1: VariantStep1Values, attributes: VariantAttributeValue[]): CreateVariantArgs {
    return {
      name: step1.name,
      productId: step1.product!.value,
      productStructureId: step1.productStructure!.value,
      attributes,
    };
  }

  static toUpdateArgs(id: ID, step1: VariantStep1Values, attributes: VariantAttributeValue[]): UpdateVariantArgs {
    return { id, name: step1.name, productId: step1.product!.value, productStructureId: step1.productStructure!.value, attributes };
  }
}
```

---

## Nhóm D — Rewrite pages

### D1. Rewrite `VariantListPage.tsx`

**Violations hiện tại:**
- `useQueryClient` + `useDeleteVariant` + manual invalidate → dùng `useVariantActions`
- `DataTable<Variant>` → `DataTable<VariantView>`
- `useGetVariantColumns({ onAction })` wrong signature → `({ handleActionClick })`
- `<Input>` search → `<ListVariantFilter>` + `<Search>`
- Hardcoded "Quản lý Variant", "Tạo Variant mới", "Xóa Variant" → `useAppTranslation()`
- Edit route: `/variants/${item.id}/edit` → `/variants/${item.id}/update`

**File đúng:**
```typescript
// presentation/components/pages/variant/ListVariant/ListVariant.tsx
export const ListVariant: FC = () => {
  const { confirm } = useConfirm();
  const navigate = useNavigate();
  const { variantText, confirmText } = useAppTranslation();

  const { page, limit, searchString, setPage, setLimit, setSearchString } =
    useAppSearch({ initialPage: 1, initialLimit: 20, initialSearchString: "" });

  const { variants, isVariantsLoading } = useGetVariants({
    page, limit, name: searchString || undefined,
  });

  const variantViews = variants?.result.map((v) => new VariantView(v)) || [];

  const { handleAction } = useVariantActions();

  const handleActionClick = (action: VariantAction, data: VariantView | null) => {
    switch (action) {
      case "edit": if (data) navigate(`/variants/${data.id}/update`); break;
      case "detail": if (data) navigate(`/variants/${data.id}`); break;
      case "delete":
        if (data?.id) confirm({
          title: confirmText.deleteTitle(variantText.title),
          description: confirmText.deleteContent(variantText.title),
          onConfirm: () => handleAction("delete", data.id),
        });
        break;
    }
  };

  const { columns } = useGetVariantColumns({ handleActionClick });

  return (
    <ListPage>
      <ListPage.Title>{variantText.title}</ListPage.Title>
      <ListPage.Filter>
        <div className="flex justify-between items-center">
          <ListVariantFilter searchString={searchString} handleSearchChange={setSearchString} />
          <Button color="secondary" size="sm" startIcon={<PlusIcon />}
            onClick={() => navigate("/variants/new")}>
            {variantText.add}
          </Button>
        </div>
      </ListPage.Filter>
      <ListPage.Main>
        <DataTable<VariantView>
          columns={columns} data={variantViews} loading={isVariantsLoading}
          page={page} limit={limit as ItemsPerPage}
          setPage={setPage} setLimit={setLimit}
          total={variants?.total || 0}
          onRowClick={(row) => handleActionClick("detail", row)}
          getRowHref={(row) => `/variants/${row.id}`}
        />
      </ListPage.Main>
    </ListPage>
  );
};
```

**Tạo thêm:** `ListVariant/ListVariantFilter.tsx` dùng `<Search>` từ `@kingstonvn/ui`.

### D2. Rewrite `ComponentTypeListPage.tsx`

**Violations hiện tại:**
- `useQueryClient` + 3x manual invalidate → `useComponentTypeActions`
- `DataTable<ComponentType>` → `DataTable<ComponentTypeView>`
- Import `CreateComponentTypeArgs, UpdateComponentTypeArgs` trực tiếp vào page
- `handleSubmit` chứa create/edit logic → nên delegate hết vào FormDialog
- Hardcoded text → `useAppTranslation()`
- Không có `useAppSearch` (không có pagination vì là config entity — OK giữ nguyên)
- `data={componentTypes}` — sau fix B2 hook, `componentTypes` là `PaginationResult | undefined`, cần `componentTypes?.result ?? []`

**Fix:**
```typescript
const { componentTypes, isComponentTypesLoading } = useGetComponentTypes();
// sau fix B2: componentTypes là PaginationResult | undefined
// → data={componentTypes?.result ?? []}
```

### D3. Tạo `DetailVariantLayout` + `DetailVariantGeneral` — xóa `VariantDetailPage.tsx`

**`presentation/components/pages/variant/DetailVariantLayout/DetailVariantLayout.tsx`**

Dùng `generate-presentation-detail` skill:
```typescript
// Wrap bằng DetailPage<VariantView>
// DetailPage.Title + DetailPage.Header + DetailPage.Main > Outlet
// useGet**VariantById**(id) + VariantView
// useVariantActions cho edit/delete
// useRouteMatch cho tabs
// useAppTranslation cho text
```

**`presentation/components/pages/variant/DetailVariantGeneral/DetailVariantGeneral.tsx`**
```typescript
// useDetailPageContext<VariantView>()
// <DetailPage.MainContent>
//   <InfoCard> thông tin cơ bản
//   Custom section cho components/attributes (read-only key-value pairs)
// </DetailPage.MainContent>
```

**Xóa:** `presentation/components/pages/variant/VariantDetailPage.tsx`

### D4. Tạo `CreateVariant` + `UpdateVariant` — xóa `VariantFormPage.tsx`

**`presentation/components/pages/variant/CreateVariant/CreateVariant.tsx`**
```typescript
// FormStateProvider + FormPage
// FormPage.Title + FormDialogActions
// Stepper state (step 1/2) giữ nguyên logic nhưng wrap đúng layout
// useVariantActions → handleAction("create", VariantFormMapper.toCreateArgs(...))
// onSuccess: navigate(`/variants/${data.id}`)
// useAppTranslation
```

**`presentation/components/pages/variant/UpdateVariant/UpdateVariant.tsx`**
```typescript
// useGetVariantById(id) để load data
// VariantFormMapper.step1ToForm(variant) cho defaultValues
// useVariantActions → handleAction("update", VariantFormMapper.toUpdateArgs(...))
// onSuccess: navigate(`/variants/${id}`)
```

**Xóa:** `presentation/components/pages/variant/VariantFormPage.tsx`

---

## Nhóm E — Routes fix

**File:** `routes/allRoutes.tsx`

**Violations:**
- Import `VariantDetailPage`, `VariantFormPage` → đổi sang components mới
- `/variants/:id/edit` → `/variants/:id/update`
- `/variants/:id` cần nested children cho `DetailVariantLayout`

**Pattern đúng:**
```typescript
{
  path: "/variants",
  component: <RequirePermission ...><ListVariant /></RequirePermission>,
},
{
  path: "/variants/new",
  component: <RequirePermission ...><CreateVariant /></RequirePermission>,
},
{
  path: "/variants/:id/update",   // ← đổi từ /edit
  component: <RequirePermission ...><UpdateVariant /></RequirePermission>,
},
{
  path: "/variants/:id",
  component: <RequirePermission ...><DetailVariantLayout /></RequirePermission>,
  children: [
    { index: true, element: <Navigate to="general" replace /> },
    { path: "general", element: <DetailVariantGeneral /> },
  ],
},
```

> **Lưu ý:** Check router setup của project — có thể dùng flat routes thay vì nested. Xem pattern từ Order routes làm reference.

---

## Thứ tự thực hiện (dependency order)

```
A1-A3 (rename useCases)
  ↓
B1-B5 (fix hooks — phụ thuộc vào useCase class names)
  ↓
C1-C5 (tạo files mới — ViewModel, FormValues, Mapper, i18n)
  ↓
D3 (DetailVariantLayout — cần VariantView từ C1, useGetVariantById từ B1)
  ↓
D4 (CreateVariant/UpdateVariant — cần FormValues C4, Mapper C5)
  ↓
D1 (ListVariant — cần VariantView C1, useVariantActions, column fix B3)
  ↓
D2 (ComponentTypeListPage — cần ComponentTypeView C2, column fix B4)
  ↓
E  (allRoutes — cần tất cả components từ D)
  ↓
bun type-check
```

---

## Checklist

### Nhóm A — UseCase naming
- [ ] A1. Rename 11 useCase files (filename + class name)
- [ ] A2. Update DI container imports
- [ ] A3. Update all hook imports

### Nhóm B — Hooks
- [ ] B1. Rename `useGetVariant` → `useGetVariantById`
- [ ] B2. Fix `useGetComponentTypes` return type (PaginationResult, không unwrap)
- [ ] B3. Fix `useGetVariantColumns` — VariantView + i18n + action pattern
- [ ] B4. Fix `useGetComponentTypeColumns` — ComponentTypeView + i18n + action pattern
- [ ] B5. Update barrel `hooks/index.ts` (variant)

### Nhóm C — New files
- [ ] C1. `viewModels/VariantView.tsx`
- [ ] C2. `viewModels/ComponentTypeView.tsx`
- [ ] C3. i18n keys (vn.json + en.json)
- [ ] C4. `forms/variant/VariantFormValues.ts`
- [ ] C5. `modules/variant/mappers.ts` (VariantFormMapper)

### Nhóm D — Rewrite pages
- [ ] D1. Rewrite `ListVariant` + tạo `ListVariantFilter`
- [ ] D2. Rewrite `ComponentTypeListPage`
- [ ] D3. Tạo `DetailVariantLayout` + `DetailVariantGeneral`, xóa `VariantDetailPage`
- [ ] D4. Tạo `CreateVariant` + `UpdateVariant`, xóa `VariantFormPage`

### Nhóm E — Routes
- [ ] E. Update `allRoutes.tsx`

### Final
- [ ] `bun type-check` pass
- [ ] `bun lint` pass
