# Agency Location Coverage API

API để quản lý vùng phủ (coverage area) của đại lý/chi nhánh.

## 🎯 Nguyên tắc quan trọng

**Mỗi Phường/Xã chỉ thuộc 1 Agency:**

- ✅ Sử dụng `Location.AgencyId` để xác định ownership (không cần bảng AgencyLocation)
- ✅ CHỈ phường/xã (Level 3) mới có `AgencyId > 0`
- ✅ Tỉnh/Quận luôn có `AgencyId = 0` (không sử dụng)
- ✅ `AgencyId = 0` nghĩa là location chưa được gán cho agency nào

**Ví dụ:**

```sql
-- Agency 1 có coverage:
UPDATE Location SET AgencyId = 1 WHERE Id = 100; -- Phường Bến Nghé
UPDATE Location SET AgencyId = 1 WHERE Id = 101; -- Phường Bến Thành

-- Hệ thống tự động suy ra:
-- → Agency 1 phủ Quận 1 (vì có phường thuộc Quận 1)
-- → Agency 1 phủ TP.HCM (vì có phường thuộc TP.HCM)

-- Phường chưa gán:
SELECT * FROM Location WHERE AgencyId = 0 AND Level = 3
```

## Endpoints

### 1. Lấy thông tin vùng phủ của đại lý

**GET** `/api/AgencyLocation/GetAgencyCoverage`

Lấy danh sách các location của đại lý, bao gồm:

- **Phường/Xã đã gán** (Level 3 - lưu trong database)
- **Quận/Huyện tương ứng** (Level 2 - suy ra từ ParentId)
- **Tỉnh/Thành phố tương ứng** (Level 1 - suy ra từ ParentId)

**Parameters:**

- `agencyId` (int, required): ID của đại lý

**Authorization:** `AgencyLocation.GetAgencyCoverage`

**Response:**

```json
{
  "agencyId": 1,
  "locations": [
    {
      "id": 1,
      "name": "Hồ Chí Minh",
      "level": 1,
      "type": "Urban"
    },
    {
      "id": 10,
      "name": "Quận 1",
      "level": 2,
      "parentId": 1,
      "type": "Urban"
    },
    {
      "id": 100,
      "name": "Bến Nghé",
      "level": 3,
      "parentId": 10,
      "type": "Urban"
    }
  ]
}
```

**⚠️ Lưu ý:**

- Database: `Location` table có `AgencyId` column
- Phường Bến Nghé có `AgencyId = 1` (thuộc Agency 1)
- API tự động query Quận 1 và TP.HCM từ ParentId của phường
- Response trả về full hierarchy (Tỉnh + Quận + Phường)

### 2. Lấy danh sách location chưa được gán (dạng cây - Lazy Loading)

**GET** `/api/AgencyLocation/GetAvailableLocationsTree`

Lấy danh sách các location chưa được gán cho đại lý dưới dạng cây hierarchical với lazy loading.

**Parameters:**

- `agencyId` (int, required): ID của đại lý
- `parentId` (int, optional, default=0): ID của location cha
  - `parentId = 0`: Lấy danh sách tỉnh/thành phố (Level 1)
  - `parentId = tỉnhId`: Lấy danh sách quận/huyện của tỉnh
  - `parentId = quậnId`: Lấy danh sách phường/xã của quận

**Filtering Logic (Quan trọng!):**

- ✅ **Phường/Xã (Level 3)**: Hiển thị nếu `AgencyId = 0` (chưa được gán)
- ✅ **Quận/Huyện (Level 2)**: Chỉ hiển thị nếu còn **ít nhất 1 phường có AgencyId = 0**
- ✅ **Tỉnh/TP (Level 1)**: Chỉ hiển thị nếu còn **ít nhất 1 quận có phường với AgencyId = 0**
- ✅ Chỉ lấy location có `type = Urban` hoặc `SubUrban`
- ❌ Không lấy location có `type = NotServed`

**Ví dụ:**

- Nếu tất cả phường/xã của Quận 1 có `AgencyId > 0` → Quận 1 sẽ KHÔNG hiển thị
- Nếu tất cả phường/xã của TP.HCM có `AgencyId > 0` → TP.HCM sẽ KHÔNG hiển thị

**Query logic:**

```sql
-- Phường available
SELECT * FROM Location WHERE AgencyId = 0 AND Level = 3

-- Quận có phường available
SELECT * FROM Location l2
WHERE l2.Level = 2
AND EXISTS (
    SELECT 1 FROM Location l3
    WHERE l3.ParentId = l2.Id
    AND l3.AgencyId = 0
    AND l3.Level = 3
)
```

**Authorization:** `AgencyLocation.GetAvailableLocationsTree`

**Response Example (parentId=0 - Lấy danh sách tỉnh):**

```json
[
  {
    "id": 1,
    "name": "Hồ Chí Minh",
    "fullName": "Thành phố Hồ Chí Minh",
    "level": 1,
    "parentId": 0,
    "type": "Urban",
    "prefix": "Thành phố",
    "displayOrder": 1,
    "hasChildren": false,
    "childrenCount": 0,
    "availableChildrenCount": 24,
    "children": []
  },
  {
    "id": 2,
    "name": "Hà Nội",
    "fullName": "Thành phố Hà Nội",
    "level": 1,
    "parentId": 0,
    "type": "Urban",
    "prefix": "Thành phố",
    "displayOrder": 2,
    "hasChildren": false,
    "childrenCount": 0,
    "availableChildrenCount": 30,
    "children": []
  }
]
```

**⚠️ Lưu ý về properties:**

- `hasChildren`: `false` vì `children` array đang empty (chưa load)
- `childrenCount`: `0` vì `children` array đang empty
- `availableChildrenCount`: Số lượng children có thể load (quan trọng!) ✅
  - Nếu > 0: Có thể gọi API với `parentId` này để load children
  - Nếu = 0: Đây là leaf node, không có children

**Response Example (parentId=1 - Lấy danh sách quận của TP.HCM):**

```json
[
  {
    "id": 10,
    "name": "Quận 1",
    "fullName": "Quận 1",
    "level": 2,
    "parentId": 1,
    "type": "Urban",
    "prefix": "Quận",
    "displayOrder": 1,
    "hasChildren": false,
    "childrenCount": 0,
    "availableChildrenCount": 10,
    "children": []
  },
  {
    "id": 11,
    "name": "Quận 2",
    "fullName": "Quận 2",
    "level": 2,
    "parentId": 1,
    "type": "SubUrban",
    "prefix": "Quận",
    "displayOrder": 2,
    "hasChildren": false,
    "childrenCount": 0,
    "availableChildrenCount": 0,
    "children": []
  }
]
```

**Giải thích:**

- Quận 1: `availableChildrenCount = 10` → Có thể load 10 phường/xã
- Quận 2: `availableChildrenCount = 0` → Không có phường/xã available (đã hết hoặc đã được assign)

**Ưu điểm so với GetDeployLocations:**

- ✅ **Lazy Loading**: Chỉ load data khi cần, không load toàn bộ cây một lúc
- ✅ **Performance**: Cực kỳ nhanh vì chỉ query 1 level tại 1 thời điểm
- ✅ **Filtering**: Tự động lọc location Urban/SubUrban, bỏ qua NotServed
- ✅ **EF Core**: Dễ bảo trì và mở rộng hơn Stored Procedure
- ✅ **Scalable**: Xử lý tốt với database lớn (hàng triệu location)
- ✅ **UX**: Cho phép progressive disclosure - user chỉ thấy những gì cần thiết

### 3. Thêm location vào đại lý

**POST** `/api/AgencyLocation/AddLocation`

Thêm một hoặc nhiều location vào vùng phủ của đại lý.

**⚠️ QUAN TRỌNG:**

- **CHỈ chấp nhận LocationId của Phường/Xã (Level 3)**
- ❌ KHÔNG chấp nhận LocationId của Tỉnh/TP hoặc Quận/Huyện
- API sẽ validate và trả về error nếu có LocationId không phải Level 3
- Location phải chưa thuộc agency nào khác (AgencyId = 0)

**Authorization:** `AgencyLocation.AddLocation`

**Request Body:**

```json
{
  "agencyId": 1,
  "locationIds": [100, 101, 102]
}
```

**⚠️ Chú ý:**

- `locationIds` chỉ chứa ID của **phường/xã** (Level 3)
- Ví dụ: [100, 101, 102] = 3 phường
- ❌ Không được chứa ID của tỉnh (Level 1) hay quận (Level 2)
- ❌ Không được chứa location đã thuộc agency này hoặc agency khác

**Success Response:**

```json
{
  "result": "Success",
  "message": "Đã thêm 3 location vào đại lý thành công"
}
```

**Error Response (khi có location không phải Level 3):**

```json
{
  "result": "Failed",
  "message": "Chỉ được phép gán phường/xã (Level 3). Phát hiện 2 location không hợp lệ: Quận 1, Hồ Chí Minh"
}
```

**Error Response (khi location đã thuộc agency khác):**

```json
{
  "result": "Failed",
  "message": "Một số location đã thuộc agency khác: Bến Nghé (Agency 2)"
}
```

**Error Response (khi location đã thuộc agency này):**

```json
{
  "result": "Failed",
  "message": "Một số location đã thuộc agency này: Bến Nghé"
}
```

### 4. Xóa location khỏi đại lý

**POST** `/api/AgencyLocation/RemoveLocation`

Xóa một hoặc nhiều location khỏi vùng phủ của đại lý.

**Authorization:** `AgencyLocation.RemoveLocation`

**Request Body:**

```json
{
  "agencyId": 1,
  "locationIds": [100, 101]
}
```

**Success Response:**

```json
{
  "result": "Success",
  "message": "Đã xóa 2 location khỏi đại lý thành công"
}
```

**Error Response (khi location không thuộc agency này):**

```json
{
  "result": "Failed",
  "message": "Một số location không thuộc agency này: Bến Nghé (AgencyId: 2)"
}
```

**Error Response (khi location không tồn tại):**

```json
{
  "result": "Failed",
  "message": "Một số location không tồn tại"
}
```

## Workflow sử dụng

### Kịch bản 1: Gán vùng phủ cho đại lý mới (Lazy Loading)

1. Gọi `GetAvailableLocationsTree(agencyId, parentId=0)` để lấy danh sách tỉnh/thành phố
2. Hiển thị danh sách tỉnh cho user
3. Khi user click mở rộng tỉnh → Gọi `GetAvailableLocationsTree(agencyId, parentId=tinhId)` để lấy quận/huyện
4. Khi user click mở rộng quận → Gọi `GetAvailableLocationsTree(agencyId, parentId=quanId)` để lấy phường/xã
5. ⭐ **User CHỈ được chọn Phường/Xã** (Level 3), không được chọn Tỉnh/Quận
6. Gọi `AddLocation` với danh sách phường/xã đã chọn
7. API sẽ validate: Tất cả locationIds phải là Level 3 và chưa thuộc agency nào

### Kịch bản 2: Thêm location mới vào đại lý

1. Gọi `GetAvailableLocationsTree(agencyId, parentId=0)` để browse location chưa được gán
2. User chọn các phường/xã cần thêm
3. Gọi `AddLocation` với danh sách location đã chọn
4. API tự động validate và thêm location vào agency

### Kịch bản 3: Xóa location khỏi đại lý

1. Gọi `GetAgencyCoverage(agencyId)` để lấy danh sách location hiện tại
2. Hiển thị danh sách location đã gán
3. User chọn các location cần xóa
4. Gọi `RemoveLocation` với danh sách location cần xóa
5. API tự động set AgencyId = 0 cho các location

### Kịch bản 4: Load nhanh với Tree Select (Ant Design)

```javascript
// Load tỉnh ban đầu
const cities = await getAvailableLocationsTree(agencyId, 0);

// Khi user expand node
const onLoadData = async (node) => {
  if (node.availableChildrenCount > 0) {
    const children = await getAvailableLocationsTree(agencyId, node.id);
    node.children = children;
  }
};

// Khi user chọn location (chỉ cho phép chọn phường/xã)
const onCheck = async (checkedKeys) => {
  const wardIds = checkedKeys.filter((id) => isWard(id)); // Lọc ra phường/xã
  await addLocation({ agencyId, locationIds: wardIds });
};
```

## Cấu trúc dữ liệu

### Location Level

- **Level 1**: Tỉnh/Thành phố
- **Level 2**: Quận/Huyện
- **Level 3**: Phường/Xã

### Location Type

- **Urban**: Nội thành - lấy trả tận nơi không mất phí
- **SubUrban**: Ngoại thành - có phụ phí lấy trả tận nơi
- **NotServed**: Chưa phục vụ

## Performance Notes

### Lazy Loading Strategy

- **Không load toàn bộ cây**: API chỉ load 1 level tại 1 thời điểm
- **On-demand loading**: Children chỉ được load khi user mở rộng node
- **Minimal queries**: Mỗi request chỉ query đúng những gì cần thiết

### Query Optimization

- Sử dụng Entity Framework Core với efficient filtering
- Index trên columns: `ParentId`, `Level`, `Type`, `DisplayOrder`
- Kết quả được sắp xếp theo `DisplayOrder > Name`
- `hasChildren` check được thực hiện bằng `AnyAsync()` - rất nhanh

### Filtering Strategy

- Chỉ query location có `type = Urban` hoặc `SubUrban`
- Tự động loại bỏ location đã được assign
- Không trả về location có `type = NotServed`

### Scalability

- Xử lý tốt với database lớn (hàng triệu records)
- Response time < 100ms cho mỗi level
- Giảm bandwidth vì không truyền toàn bộ cây
- Memory efficient - không cache toàn bộ tree trong memory

## Error Handling

- `AgencyId <= 0`: Trả về BadRequest
- Agency không tồn tại: Trả về message trong BaseResult
- Location không tồn tại: Trả về message "Một số location không tồn tại"
- Exception: Trả về Result.SystemError với error message
