# Ví dụ tích hợp Agency Location API với Frontend (Lazy Loading)

## 1. React/TypeScript với Ant Design Tree Select (Lazy Loading)

```typescript
import React, { useState, useEffect } from "react";
import { TreeSelect, Button, message } from "antd";
import { agencyLocationApi } from "./api";

interface LocationTreeNode {
  id: number;
  name: string;
  fullName: string;
  level: number;
  parentId: number;
  type: string;
  prefix: string;
  displayOrder: number;
  hasChildren: boolean;
  childrenCount: number;
  availableChildrenCount: number; // Số lượng children có thể load
  children: LocationTreeNode[];
}

interface Props {
  agencyId: number;
}

const AgencyLocationSelector: React.FC<Props> = ({ agencyId }) => {
  const [treeData, setTreeData] = useState<any[]>([]);
  const [selectedIds, setSelectedIds] = useState<number[]>([]);
  const [loading, setLoading] = useState(false);

  // Load danh sách tỉnh/thành phố ban đầu
  useEffect(() => {
    loadCities();
  }, [agencyId]);

  const loadCities = async () => {
    setLoading(true);
    try {
      // Load tỉnh/thành phố (parentId = 0)
      const cities = await agencyLocationApi.getAvailableLocationsTree(
        agencyId,
        0
      );
      const formattedTree = convertToTreeSelectFormat(cities);
      setTreeData(formattedTree);
    } catch (error) {
      message.error("Không thể tải dữ liệu");
    } finally {
      setLoading(false);
    }
  };

  // Convert LocationTreeNode sang format của Ant Design
  const convertToTreeSelectFormat = (nodes: LocationTreeNode[]): any[] => {
    return nodes.map((node) => ({
      title: `${node.fullName}${
        node.availableChildrenCount > 0
          ? ` (${node.availableChildrenCount})`
          : ""
      }`,
      value: node.id,
      key: node.id,
      isLeaf: node.availableChildrenCount === 0, // Đánh dấu leaf node dựa vào availableChildrenCount
      level: node.level,
      type: node.type,
      selectable: true,
      availableChildrenCount: node.availableChildrenCount,
      children: node.availableChildrenCount > 0 ? [] : undefined, // Empty array để trigger lazy load
    }));
  };

  // Lazy load children khi user mở rộng node
  const onLoadData = async (treeNode: any) => {
    const { key } = treeNode;

    try {
      // Load children của node này
      const children = await agencyLocationApi.getAvailableLocationsTree(
        agencyId,
        key
      );

      const formattedChildren = convertToTreeSelectFormat(children);

      // Update tree data với children mới load
      setTreeData((prevTree) =>
        updateTreeData(prevTree, key, formattedChildren)
      );
    } catch (error) {
      message.error("Không thể tải dữ liệu con");
    }
  };

  // Helper function để update tree data
  const updateTreeData = (list: any[], key: number, children: any[]): any[] => {
    return list.map((node) => {
      if (node.key === key) {
        return { ...node, children };
      }
      if (node.children) {
        return {
          ...node,
          children: updateTreeData(node.children, key, children),
        };
      }
      return node;
    });
  };

  const handleSave = async () => {
    if (selectedIds.length === 0) {
      message.warning("Vui lòng chọn ít nhất một location");
      return;
    }

    setLoading(true);
    try {
      const result = await agencyLocationApi.updateAgencyCoverage({
        agencyId,
        locationIds: selectedIds,
      });

      if (result.result === "Success") {
        message.success("Cập nhật vùng phủ thành công");
        loadCities(); // Reload data
      } else {
        message.error(result.message);
      }
    } catch (error) {
      message.error("Có lỗi xảy ra khi cập nhật");
    } finally {
      setLoading(false);
    }
  };

  return (
    <div>
      <TreeSelect
        style={{ width: "100%" }}
        value={selectedIds}
        onChange={setSelectedIds}
        treeData={treeData}
        loadData={onLoadData}
        treeCheckable
        showCheckedStrategy={TreeSelect.SHOW_ALL}
        placeholder="Chọn tỉnh, quận, phường..."
        loading={loading}
        maxTagCount={5}
        treeDefaultExpandAll={false}
      />
      <Button
        type="primary"
        onClick={handleSave}
        loading={loading}
        style={{ marginTop: 16 }}
      >
        Lưu vùng phủ ({selectedIds.length} location)
      </Button>
    </div>
  );
};

export default AgencyLocationSelector;
```

## 2. API Service Layer (TypeScript)

```typescript
// api/agencyLocationApi.ts
import axios from "axios";

const BASE_URL = "/api/AgencyLocation";

export interface LocationItem {
  id: number;
  name: string;
  code: string;
  prefix: string;
  level: number;
  combinedName: string;
  type: string;
}

export interface AgencyCoverageResponse {
  agencyId: number;
  locations: LocationItem[];
}

export interface LocationTreeNode {
  id: number;
  name: string;
  fullName: string;
  level: number;
  parentId: number;
  type: string;
  prefix: string;
  displayOrder: number;
  hasChildren: boolean;
  childrenCount: number;
  availableChildrenCount: number; // Số lượng children có thể load
  children: LocationTreeNode[];
}

export interface UpdateCoverageRequest {
  agencyId: number;
  locationIds: number[];
}

export interface BaseResult {
  result: "Success" | "Failed" | "SystemError";
  message: string;
}

export const agencyLocationApi = {
  // Lấy vùng phủ hiện tại
  getAgencyCoverage: async (
    agencyId: number
  ): Promise<AgencyCoverageResponse> => {
    const response = await axios.get(`${BASE_URL}/GetAgencyCoverage`, {
      params: { agencyId },
    });
    return response.data;
  },

  // Lấy danh sách location chưa gán (lazy loading)
  getAvailableLocationsTree: async (
    agencyId: number,
    parentId: number = 0
  ): Promise<LocationTreeNode[]> => {
    const response = await axios.get(`${BASE_URL}/GetAvailableLocationsTree`, {
      params: { agencyId, parentId },
    });
    return response.data;
  },

  // Cập nhật vùng phủ
  updateAgencyCoverage: async (
    request: UpdateCoverageRequest
  ): Promise<BaseResult> => {
    const response = await axios.post(
      `${BASE_URL}/UpdateAgencyCoverage`,
      request
    );
    return response.data;
  },
};
```

## 3. Vue 3 với Element Plus Cascader (Lazy Loading)

```vue
<template>
  <div class="agency-location-selector">
    <el-cascader
      v-model="selectedValues"
      :options="options"
      :props="cascaderProps"
      :show-all-levels="false"
      collapse-tags
      clearable
      filterable
      placeholder="Chọn tỉnh, quận, phường..."
      style="width: 100%"
    />
    <el-button
      type="primary"
      @click="handleSave"
      :loading="loading"
      style="margin-top: 16px"
    >
      Lưu vùng phủ ({{ selectedLocationIds.length }} location)
    </el-button>
  </div>
</template>

<script setup lang="ts">
  import { ref, computed, onMounted } from "vue";
  import { ElMessage } from "element-plus";
  import { agencyLocationApi, LocationTreeNode } from "./api";

  interface Props {
    agencyId: number;
  }

  const props = defineProps<Props>();

  const options = ref<LocationTreeNode[]>([]);
  const selectedValues = ref<number[][]>([]);
  const loading = ref(false);

  // Computed để lấy tất cả locationIds từ selected paths
  const selectedLocationIds = computed(() => {
    return selectedValues.value.map((path) => path[path.length - 1]);
  });

  const cascaderProps = {
    value: "id",
    label: "fullName",
    children: "children",
    checkStrictly: true, // Cho phép chọn bất kỳ level nào
    multiple: true,
    lazy: true, // Enable lazy loading
    lazyLoad: async (node: any, resolve: any) => {
      const parentId = node.value || 0;

      try {
        const children = await agencyLocationApi.getAvailableLocationsTree(
          props.agencyId,
          parentId
        );

        // Đánh dấu leaf nodes dựa vào availableChildrenCount
        const formattedChildren = children.map((child) => ({
          ...child,
          label: `${child.fullName}${
            child.availableChildrenCount > 0
              ? ` (${child.availableChildrenCount})`
              : ""
          }`,
          leaf: child.availableChildrenCount === 0,
        }));

        resolve(formattedChildren);
      } catch (error) {
        ElMessage.error("Không thể tải dữ liệu");
        resolve([]);
      }
    },
  };

  onMounted(async () => {
    loading.value = true;
    try {
      // Load tỉnh/thành phố ban đầu
      const cities = await agencyLocationApi.getAvailableLocationsTree(
        props.agencyId,
        0
      );
      options.value = cities.map((city) => ({
        ...city,
        label: `${city.fullName}${
          city.availableChildrenCount > 0
            ? ` (${city.availableChildrenCount})`
            : ""
        }`,
        leaf: city.availableChildrenCount === 0,
      }));
    } catch (error) {
      ElMessage.error("Không thể tải dữ liệu");
    } finally {
      loading.value = false;
    }
  });

  const handleSave = async () => {
    if (selectedLocationIds.value.length === 0) {
      ElMessage.warning("Vui lòng chọn ít nhất một location");
      return;
    }

    loading.value = true;
    try {
      const result = await agencyLocationApi.updateAgencyCoverage({
        agencyId: props.agencyId,
        locationIds: selectedLocationIds.value,
      });

      if (result.result === "Success") {
        ElMessage.success("Cập nhật vùng phủ thành công");
      } else {
        ElMessage.error(result.message);
      }
    } catch (error) {
      ElMessage.error("Có lỗi xảy ra khi cập nhật");
    } finally {
      loading.value = false;
    }
  };
</script>
```

## 4. React Hook tùy chỉnh cho Lazy Loading

```typescript
// hooks/useLocationTree.ts
import { useState, useCallback } from "react";
import { agencyLocationApi, LocationTreeNode } from "../api";

interface TreeNode extends LocationTreeNode {
  isLeaf: boolean; // Dựa vào availableChildrenCount === 0
  children: TreeNode[];
}

export const useLocationTree = (agencyId: number) => {
  const [treeData, setTreeData] = useState<TreeNode[]>([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  // Load initial cities
  const loadInitial = useCallback(async () => {
    setLoading(true);
    setError(null);
    try {
      const cities = await agencyLocationApi.getAvailableLocationsTree(
        agencyId,
        0
      );
      setTreeData(
        cities.map((city) => ({
          ...city,
          isLeaf: city.availableChildrenCount === 0,
          children: [],
        }))
      );
    } catch (err) {
      setError("Không thể tải danh sách tỉnh/thành phố");
      console.error(err);
    } finally {
      setLoading(false);
    }
  }, [agencyId]);

  // Load children for a node
  const loadChildren = useCallback(
    async (parentId: number): Promise<TreeNode[]> => {
      try {
        const children = await agencyLocationApi.getAvailableLocationsTree(
          agencyId,
          parentId
        );
        return children.map((child) => ({
          ...child,
          isLeaf: child.availableChildrenCount === 0,
          children: [],
        }));
      } catch (err) {
        console.error("Error loading children:", err);
        return [];
      }
    },
    [agencyId]
  );

  // Update tree with loaded children
  const updateNode = useCallback((nodeId: number, children: TreeNode[]) => {
    const updateRecursive = (nodes: TreeNode[]): TreeNode[] => {
      return nodes.map((node) => {
        if (node.id === nodeId) {
          return { ...node, children };
        }
        if (node.children.length > 0) {
          return { ...node, children: updateRecursive(node.children) };
        }
        return node;
      });
    };
    setTreeData((prev) => updateRecursive(prev));
  }, []);

  return {
    treeData,
    loading,
    error,
    loadInitial,
    loadChildren,
    updateNode,
  };
};

// Usage
const MyComponent = ({ agencyId }: { agencyId: number }) => {
  const { treeData, loading, loadInitial, loadChildren, updateNode } =
    useLocationTree(agencyId);

  useEffect(() => {
    loadInitial();
  }, [loadInitial]);

  const handleLoadData = async (node: any) => {
    const children = await loadChildren(node.id);
    updateNode(node.id, children);
  };

  // ... render tree with treeData
};
```

## 5. Performance Tips

### Caching Strategy

```typescript
// Simple in-memory cache
const locationCache = new Map<string, LocationTreeNode[]>();

export const getCachedLocations = async (
  agencyId: number,
  parentId: number
): Promise<LocationTreeNode[]> => {
  const cacheKey = `${agencyId}-${parentId}`;

  if (locationCache.has(cacheKey)) {
    return locationCache.get(cacheKey)!;
  }

  const locations = await agencyLocationApi.getAvailableLocationsTree(
    agencyId,
    parentId
  );

  locationCache.set(cacheKey, locations);
  return locations;
};

// Clear cache khi update
export const clearLocationCache = () => {
  locationCache.clear();
};
```

### Debounced Search

```typescript
import { useMemo } from "react";
import debounce from "lodash/debounce";

const LocationSearch = ({ agencyId }: { agencyId: number }) => {
  const [searchResults, setSearchResults] = useState<LocationTreeNode[]>([]);

  const debouncedSearch = useMemo(
    () =>
      debounce(async (keyword: string) => {
        if (keyword.length < 2) {
          setSearchResults([]);
          return;
        }

        // Search logic - có thể cần API endpoint riêng cho search
        // Hoặc load all và filter client-side
      }, 300),
    [agencyId]
  );

  return (
    <input
      type="text"
      onChange={(e) => debouncedSearch(e.target.value)}
      placeholder="Tìm kiếm location..."
    />
  );
};
```

## 6. Testing

```typescript
// locationTree.test.ts
import { renderHook, waitFor } from "@testing-library/react";
import { useLocationTree } from "./useLocationTree";
import { agencyLocationApi } from "../api";

jest.mock("../api");

describe("useLocationTree", () => {
  it("should load initial cities", async () => {
    const mockCities = [
      {
        id: 1,
        name: "Hồ Chí Minh",
        fullName: "Thành phố Hồ Chí Minh",
        hasChildren: true,
        // ... other fields
      },
    ];

    (
      agencyLocationApi.getAvailableLocationsTree as jest.Mock
    ).mockResolvedValue(mockCities);

    const { result } = renderHook(() => useLocationTree(1));

    result.current.loadInitial();

    await waitFor(() => {
      expect(result.current.treeData).toHaveLength(1);
      expect(result.current.treeData[0].name).toBe("Hồ Chí Minh");
    });
  });

  it("should load children for a node", async () => {
    const mockDistricts = [
      {
        id: 10,
        name: "Quận 1",
        parentId: 1,
        hasChildren: true,
        // ... other fields
      },
    ];

    (
      agencyLocationApi.getAvailableLocationsTree as jest.Mock
    ).mockResolvedValue(mockDistricts);

    const { result } = renderHook(() => useLocationTree(1));

    const children = await result.current.loadChildren(1);

    expect(children).toHaveLength(1);
    expect(children[0].name).toBe("Quận 1");
  });
});
```

## Notes

1. **Lazy Loading**: Load data theo từng cấp, chỉ khi cần
2. **Performance**: Cực kỳ nhanh vì chỉ load những gì user đang xem
3. **availableChildrenCount**: ⭐ Property quan trọng
   - Cho biết có bao nhiêu children có thể load
   - Sử dụng để quyết định có nên lazy load hay không
   - Hiển thị số lượng trong UI: `Hà Nội (30)` = có 30 quận/huyện
   - `availableChildrenCount = 0` → leaf node, không có children
4. **Caching**: Có thể implement cache để tránh load lại data đã load
5. **Filtering**: API đã filter sẵn Urban/SubUrban, không cần filter client-side
6. **UX**: Progressive disclosure - user không bị overwhelm bởi quá nhiều data
7. **Error Handling**: Xử lý gracefully khi load fail
8. **Accessibility**: Đảm bảo keyboard navigation và screen reader support
