# Report: StructureMaterial Module — Audit & Fix

> Branch: `feature/structure-material`  
> Scope: toàn bộ module `structureMaterial` + entity, mapper, use cases, DI, và `MaterialSelect` component

---

## 1. Domain Layer

### `domain/entities/StructureMaterial.ts` — **THIẾU** (tạo mới)

| Vấn đề | Fix |
|--------|-----|
| `StructureMaterial` được định nghĩa inline trong repository file, vi phạm nguyên tắc tách entity thành file riêng | Tạo `domain/entities/StructureMaterial.ts` với đầy đủ `IStructureMaterial`, class `StructureMaterial`, `StructureMaterialBuilder`, và nested interfaces `StructureMaterialUnit`, `StructureMaterialMaterial` |

**Template áp dụng:** `domain/entities/Material.ts`, `domain/entities/ProductStructure.ts` — interface + class implements + Builder extends `Builder<T>`.

---

### `domain/repositories/StructureMaterialRepository.ts`

| Vấn đề | Fix |
|--------|-----|
| `StructureMaterial` interface định nghĩa inline, không import từ entity | Import và re-export từ `domain/entities/StructureMaterial` |
| Repository methods nhận nhiều params rời: `createStructureMaterial(structureId, input)`, `updateStructureMaterial(structureId, id, input)`, `deleteStructureMaterial(structureId, id)` | Đổi thành args object: `createStructureMaterial(args: CreateStructureMaterialArgs)`, v.v. — nhất quán với `Args` interfaces đã định nghĩa trong cùng file |

---

## 2. Application Layer (Use Cases)

### `CreateStructureMaterial.ts` / `UpdateStructureMaterial.ts` / `DeleteStructureMaterial.ts`

| Vấn đề | Fix |
|--------|-----|
| `execute(structureId, input)` — unpack params thủ công, không dùng `Args` type | `execute(args: CreateStructureMaterialArgs)` → `repository.createStructureMaterial(args)` |
| `execute(structureId, id, input)` | `execute(args: UpdateStructureMaterialArgs)` → `repository.updateStructureMaterial(args)` |
| `execute(structureId, id)` | `execute(args: DeleteStructureMaterialArgs)` → `repository.deleteStructureMaterial(args)` |
| Import `StructureMaterialInput` / `StructureMaterialUpdateInput` trực tiếp — không dùng `Args` type | Đổi import sang `CreateStructureMaterialArgs`, `UpdateStructureMaterialArgs`, `DeleteStructureMaterialArgs` |

**Template áp dụng:** Use case chỉ có `execute(input)` — một tham số duy nhất, không unpack.

---

## 3. Infrastructure Layer

### `infrastructure/api/StructureMaterialApiRepository.ts`

| Vấn đề | Fix |
|--------|-----|
| Import path sai: `"./services/ApiService"` | Sửa thành `"../services/ApiService"` |
| Method signatures dùng params rời thay vì `Args` object | Cập nhật implement theo interface mới: `createStructureMaterial(args)`, unpack `args.structureId`, `args.input` bên trong |

### `infrastructure/api/mapper/StructureMaterialMapper.ts`

| Vấn đề | Fix |
|--------|-----|
| Các numeric ID từ API response (`id`, `structureId`, `materialId`, `material.id`, `material.unit.id`) không được convert sang `ID` (`string`) | Thêm `.toString()` cho tất cả ID fields |
| Unit mapping: `response.material.unit ?? null` — gán trực tiếp `StructureMaterialUnitResponse` vào domain type | Fix thành explicit object `{ id: response.material.unit.id.toString(), name: response.material.unit.name }` |

### `infrastructure/api/types/StructureMaterialResponse.ts`

| Vấn đề | Fix |
|--------|-----|
| `StructureMaterialUnitResponse.id: number` trong khi domain dùng `ID` (`string`) | Đổi thành `id: ID` |

### `infrastructure/api/message/StructureMaterialMessage.ts` — **THIẾU** (tạo mới)

| Vấn đề | Fix |
|--------|-----|
| Không có error messages cho domain `structureMaterial` | Tạo `StructureMaterialMessageKey` enum + `StructureMaterialMessages` record theo template `MaterialMessage.ts` |
| `ErrorService` không biết domain `"structureMaterial"` | Thêm vào `Domain` union type và `DOMAIN_MESSAGES` map |

---

## 4. Presentation Layer — Hooks

### `useGetStructureMaterials.ts` — Rewrite

| Vấn đề | Fix |
|--------|-----|
| Dùng `useInjection` từ `inversify-react` (package không tồn tại trong project) | Thay bằng `container.get<T>(TYPES.X)` — pattern chuẩn toàn codebase |
| Thiếu `AuthService` guard (`enabled`) | Thêm `enabled: authService.isAuthenticated() && !!structureId` |
| Thiếu `opts?: UseQueryOptionsCustom<T>` param | Thêm param và merge vào `useQuery` |
| Return sai tên: không nhất quán với convention | Đổi thành `structureMaterials`, `isStructureMaterialsLoading`, `error`, `refetchStructureMaterials` |
| Thiếu `STRUCTURE_MATERIALS_QUERY_KEY` constant | Tạo constant dùng chung cho query key và invalidate |

**Template áp dụng:** `useGetMaterials.ts` — `container.get`, `AuthService` guard, `useQuery` với generics explicit.

### `useCreateStructureMaterial.ts` / `useUpdateStructureMaterial.ts` / `useDeleteStructureMaterial.ts` — **THIẾU** (tạo mới)

| Vấn đề | Fix |
|--------|-----|
| Không có individual mutation hooks — mọi thứ nhét vào một hook duy nhất | Tách thành 3 hooks riêng theo template `useCreateMaterial`, `useUpdateMaterial`, `useDeleteMaterial` |
| `wrapMutationFn` gọi với `(args) => useCase.execute(args.x, args.y)` | Simplify thành `(args) => useCase.execute(args)` sau khi use case nhận args object |

**Template áp dụng:** `useCreateMaterial.ts` — `useMutation`, `wrapMutationFn`, `mutationKey`, `onError` với `notificationService`.

### `useStructureMaterialActions.ts` — Rewrite

| Vấn đề | Fix |
|--------|-----|
| Tự implement mutation logic thủ công — không dùng `useEntityAction` | Rewrite dùng `useEntityAction` với `actionsMap` và `getInvalidateKeys` |
| `getInvalidateKeys` trả về key cứng | Trả về `STRUCTURE_MATERIALS_QUERY_KEY(args.structureId)` — dynamic theo `structureId` |

**Template áp dụng:** `useMaterialActions.ts`.

### `hooks/index.ts` — **THIẾU** (tạo mới)

Tạo barrel export cho tất cả hooks trong module.

---

## 5. Presentation Layer — Components

### `StructureMaterialActionDropdown` — **THIẾU** (tạo mới)

| Vấn đề | Fix |
|--------|-----|
| Không có action dropdown cho table rows | Tạo theo template `MaterialActionDropdown` — `DropdownMenu` + `EditIcon` + `TrashIcon` + export `StructureMaterialAction = "edit" \| "delete"` |

### `useGetStructureMaterialColumns.tsx` — **THIẾU** (tạo mới)

| Vấn đề | Fix |
|--------|-----|
| Columns định nghĩa inline trong table component | Tách thành `useGetStructureMaterialColumns` hook riêng với `ColumnDef<StructureMaterial>[]` + `useMemo` |

**Template áp dụng:** `useGetMaterialColumns.tsx`.

### `StructureMaterialTable.tsx` — Rewrite

| Vấn đề | Fix |
|--------|-----|
| Columns inline, không dùng column hook | Dùng `useGetStructureMaterialColumns` |
| Props `onEdit`/`onDelete` rời — không theo action pattern | Gộp thành `onAction: (action, item) => void` |
| `emptyText` không phải prop của `DataTable` | Xóa, thêm `isHiddenPagination` |

### `StructureMaterialFormModal.tsx` — Rewrite

| Vấn đề | Fix |
|--------|-----|
| Không tách inner form component | Tách `StructureMaterialForm` riêng để dùng `useFormStateContext` |
| Thiếu sync `isSubmitting`/`isDirty`/`isValid` lên modal submit button | Thêm `useEffect` sync `setFormState({ isDirty, isValid, isSubmitting })` |
| `Dialog` dùng sai prop: `onOpenChange` (không tồn tại) | Đổi thành `setOpen={onCancel}` |
| `Dialog.Footer` không tồn tại | Dùng `actions={<FormDialogActions formId={FORM_ID} onCancel={onCancel} />}` |
| `FormStateProvider` không nhận `isSubmitting` prop | `FormStateProvider` chỉ nhận `children` — sync state qua `useFormStateContext` |
| Thiếu `mode: "onChange"` trong `useForm` | `isValid` không reactive → submit button luôn bị disable |
| `materialId` field dùng plain `Input` | Thay bằng `MaterialSelect` — infinite scroll select với search |
| Resolver type error với conditional schema | Dùng double cast: `yupResolver(schema as yup.ObjectSchema<T>) as Resolver<T>` |

### `DetailProductStructureMaterials.tsx`

| Vấn đề | Fix |
|--------|-----|
| Thiếu `DetailPageFilter` và `DetailPageMainContent` layout wrappers | Thêm `<DetailPageFilter>` (conditional theo `canEdit`) + `<DetailPageMainContent noPadding>` |
| Dùng `useHasPermission` (không tồn tại) | Đổi thành `usePermission().can` |
| Closing tag mismatch `</div>` → `</>` | Fix fragment |
| `handleCreate` / `handleUpdate` không dùng `overrideConfig.onSuccess` để đóng modal | Thêm `overrideConfig: { onSuccess: () => setIsAddOpen(false) }` |

---

## 6. Form Values

### `forms/structureMaterial/StructureMaterialFormValues.ts`

| Vấn đề | Fix |
|--------|-----|
| `materialId: ID \| null` — plain string, không thể bind với `react-select` value | Đổi thành `materialId: OptionSelect<ID> \| null` — full option object |
| Schema: `yup.string().required()` — không match kiểu mới | Đổi thành `yup.mixed<OptionSelect<ID>>().required("Vui lòng chọn vật tư")` |
| `handleCreate` dùng `values.materialId!` trực tiếp | Đổi thành `values.materialId!.value` để lấy ID string |

---

## 7. Material Module — Bổ sung

### `material/hooks/useGetMaterialsInfinite.ts` — **THIẾU** (tạo mới)

`MaterialSelect` cần infinite query hook. Tạo theo template `useGetBomsInfinite` — `useBaseInfiniteQuery` với `TYPES.GetMaterials`.

### `material/components/MaterialSelect/` — **THIẾU** (tạo mới)

| Vấn đề | Fix |
|--------|-----|
| Không có `MaterialSelect` component | Tạo với `GenericInfiniteSelect<Material>`, open/close state, search state, `queryKeysToClearOnClose` |
| `retrieveKeys` dùng `{ [Property in RetrieveKeys]?: unknown }` → type unsafe | Đổi thành `Partial<GetMaterialsParams>` |

---

## Tổng hợp lỗi theo pattern

| Pattern vi phạm | Số lần |
|----------------|--------|
| Dùng `useInjection` (không tồn tại) thay vì `container.get` | 4 hooks |
| Repository / use case nhận params rời thay vì `Args` object | 3 methods × 2 layers |
| Entity định nghĩa inline trong repository | 1 |
| Thiếu `AuthService` guard trong `useQuery` | 1 |
| Thiếu individual mutation hooks, nhét vào 1 hook | 1 |
| Không dùng `useEntityAction` cho actions orchestration | 1 |
| Columns inline thay vì `useGet*Columns` hook | 1 |
| `Dialog` sai props (`onOpenChange`, `Dialog.Footer`) | 1 |
| Thiếu `mode: "onChange"` → `isValid` không reactive | 1 |
| `FormStateProvider` sync sai cách | 1 |
| Thiếu `DetailPageFilter`/`DetailPageMainContent` layout | 1 |
| Numeric ID từ API không `.toString()` trong mapper | 5 fields |
| Form value type không match select component | 1 |
