# API: Báo Cáo Đơn Hàng Theo Tuyến

## Thông Tin Chung

| Thuộc tính | Giá trị |
|------------|---------|
| **Endpoint** | `POST /api/Report/OrderReportByRoute` |
| **Controller** | `ReportController` |
| **Method** | `OrderReportByRoute` |
| **Authorization** | Required (JWT Bearer Token) |
| **Permission** | `Report.OrderReportByRoute` |
| **Stored Procedure** | `sp_GetOrderReportByRoute` |

## Mô Tả

API này trả về báo cáo tổng hợp số lượng đơn hàng và doanh số theo tuyến vận chuyển (từ tỉnh/thành phố gửi đến tỉnh/thành phố nhận) và trạng thái đơn hàng.

### Tính Năng Chính

- ✅ Tổng hợp đơn hàng theo tuyến (Tỉnh gửi - Tỉnh nhận)
- ✅ Phân loại theo 6 trạng thái chính
- ✅ Tính toán doanh số (tổng, trung bình, min, max)
- ✅ Lọc theo khoảng thời gian
- ✅ Lọc theo Sale (chỉ Founder)
- ✅ Lọc theo tỉnh/thành phố gửi và nhận
- ✅ Tự động lọc theo user nếu không phải Founder

## Authentication & Authorization

### JWT Token Required

```http
Authorization: Bearer {your_jwt_token}
```

### Permission Required

User cần có permission: **`Report.OrderReportByRoute`**

### Role-Based Access Control

| Role | Quyền xem |
|------|-----------|
| **Founder** | Xem tất cả đơn hàng, có thể lọc theo bất kỳ Sale nào |
| **User thường** | Chỉ xem đơn hàng của chính mình (tự động lọc theo UserId) |

## Request

### HTTP Method & URL

```http
POST /api/Report/OrderReportByRoute
Content-Type: application/json
```

### Request Body Structure (SmartTableParam)

```json
{
  "Pagination": {
    "PageIndex": 1,
    "PageSize": 100
  },
  "Sort": {
    "Predicate": "",
    "Reverse": false
  },
  "Search": {
    "PredicateObject": {
      "StartDate": "DateTimeOffset",
      "EndDate": "DateTimeOffset",
      "SaleId": "int? (optional, chỉ Founder)",
      "SourceCityIds": "array<int>? (optional)",
      "DestCityIds": "array<int>? (optional)"
    }
  }
}
```

### Request Parameters

#### Pagination (Required)
| Tham số | Kiểu | Bắt buộc | Mô tả | Mặc định |
|---------|------|----------|-------|----------|
| `PageIndex` | int | **Có** | Số trang hiện tại | 1 |
| `PageSize` | int | **Có** | Số bản ghi mỗi trang | 100 |

#### Sort (Required)
| Tham số | Kiểu | Bắt buộc | Mô tả | Mặc định |
|---------|------|----------|-------|----------|
| `Predicate` | string | **Có** | Tên cột sắp xếp | "" (empty) |
| `Reverse` | bool | **Có** | Sắp xếp ngược | false |

#### Search.PredicateObject (Optional)
| Tham số | Kiểu | Bắt buộc | Mô tả | Mặc định |
|---------|------|----------|-------|----------|
| `StartDate` | DateTimeOffset | Không | Ngày bắt đầu lọc | Ngày đầu tháng hiện tại |
| `EndDate` | DateTimeOffset | Không | Ngày kết thúc lọc | Ngày cuối tháng hiện tại |
| `SaleId` | int | Không | ID của Sale (chỉ Founder) | null (User thường: UserId) |
| `SourceCityIds` | array<int> | Không | Danh sách ID tỉnh/thành phố gửi hàng | null (tất cả) |
| `DestCityIds` | array<int> | Không | Danh sách ID tỉnh/thành phố nhận hàng | null (tất cả) |

### Request Examples

#### Example 1: Lấy báo cáo tháng hiện tại (tất cả tuyến)

```json
{
  "Pagination": {
    "PageIndex": 1,
    "PageSize": 100
  },
  "Sort": {
    "Predicate": "",
    "Reverse": false
  },
  "Search": {
    "PredicateObject": {}
  }
}
```

#### Example 2: Lọc theo khoảng thời gian

```json
{
  "Pagination": {
    "PageIndex": 1,
    "PageSize": 100
  },
  "Sort": {
    "Predicate": "",
    "Reverse": false
  },
  "Search": {
    "PredicateObject": {
      "StartDate": "2025-01-01T00:00:00+07:00",
      "EndDate": "2025-01-31T23:59:59+07:00"
    }
  }
}
```

#### Example 3: Lọc theo tuyến cụ thể (Hà Nội -> TP.HCM)

```json
{
  "Pagination": {
    "PageIndex": 1,
    "PageSize": 100
  },
  "Sort": {
    "Predicate": "",
    "Reverse": false
  },
  "Search": {
    "PredicateObject": {
      "StartDate": "2025-01-01T00:00:00+07:00",
      "EndDate": "2025-01-31T23:59:59+07:00",
      "SourceCityIds": [1],
      "DestCityIds": [79]
    }
  }
}
```

#### Example 4: Founder lọc theo Sale cụ thể

```json
{
  "Pagination": {
    "PageIndex": 1,
    "PageSize": 100
  },
  "Sort": {
    "Predicate": "",
    "Reverse": false
  },
  "Search": {
    "PredicateObject": {
      "StartDate": "2025-01-01T00:00:00+07:00",
      "EndDate": "2025-01-31T23:59:59+07:00",
      "SaleId": 123
    }
  }
}
```

#### Example 5: Lọc nhiều tỉnh gửi và nhận

```json
{
  "Pagination": {
    "PageIndex": 1,
    "PageSize": 100
  },
  "Sort": {
    "Predicate": "",
    "Reverse": false
  },
  "Search": {
    "PredicateObject": {
      "StartDate": "2025-01-01T00:00:00+07:00",
      "EndDate": "2025-01-31T23:59:59+07:00",
      "SourceCityIds": [1, 2, 3],
      "DestCityIds": [79, 48]
    }
  }
}
```

#### Example 6: Kết hợp tất cả điều kiện

```json
{
  "Pagination": {
    "PageIndex": 1,
    "PageSize": 100
  },
  "Sort": {
    "Predicate": "TotalRevenue",
    "Reverse": true
  },
  "Search": {
    "PredicateObject": {
      "StartDate": "2025-01-01T00:00:00+07:00",
      "EndDate": "2025-01-31T23:59:59+07:00",
      "SaleId": 123,
      "SourceCityIds": [1, 2],
      "DestCityIds": [79]
    }
  }
}
```

## Response

### Success Response (200 OK)

```json
{
  "Result": 1,
  "Message": null,
  "Items": [
    {
      "RouteName": "Hà Nội - TP. Hồ Chí Minh",
      "SourceCityName": "Hà Nội",
      "DestCityName": "TP. Hồ Chí Minh",
      "OrderStatusName": "Chờ lấy hàng",
      "StatusOrder": 1,
      "OrderCount": 15,
      "TotalRevenue": 4500000,
      "AvgRevenue": 300000,
      "MinRevenue": 150000,
      "MaxRevenue": 500000,
      "FromDate": "2025-01-01T00:00:00+07:00",
      "ToDate": "2025-01-31T23:59:59+07:00"
    },
    {
      "RouteName": "Hà Nội - TP. Hồ Chí Minh",
      "SourceCityName": "Hà Nội",
      "DestCityName": "TP. Hồ Chí Minh",
      "OrderStatusName": "Đang lấy hàng",
      "StatusOrder": 2,
      "OrderCount": 8,
      "TotalRevenue": 2400000,
      "AvgRevenue": 300000,
      "MinRevenue": 200000,
      "MaxRevenue": 450000,
      "FromDate": "2025-01-01T00:00:00+07:00",
      "ToDate": "2025-01-31T23:59:59+07:00"
    }
  ],
  "TotalRecord": 2
}
```

### Response Fields

| Field | Kiểu | Mô tả |
|-------|------|-------|
| `Result` | int | Mã kết quả (1: Success - Thành công, 2: Failed - Thất bại, 3: SystemError - Lỗi hệ thống) |
| `Message` | string | Thông báo lỗi (nếu có) |
| `Items` | array | Danh sách báo cáo |
| `TotalRecord` | int | Tổng số bản ghi |

### Item Fields

| Field | Kiểu | Mô tả |
|-------|------|-------|
| `RouteName` | string | Tên tuyến: "Tỉnh gửi - Tỉnh nhận" |
| `SourceCityName` | string | Tên tỉnh/thành phố gửi hàng |
| `DestCityName` | string | Tên tỉnh/thành phố nhận hàng |
| `OrderStatusName` | string | Tên trạng thái đơn hàng |
| `StatusOrder` | int | Thứ tự sắp xếp trạng thái (1-6) |
| `OrderCount` | int | Số lượng đơn hàng |
| `TotalRevenue` | decimal | Tổng doanh số |
| `AvgRevenue` | decimal | Doanh số trung bình |
| `MinRevenue` | decimal | Doanh số thấp nhất |
| `MaxRevenue` | decimal | Doanh số cao nhất |
| `FromDate` | DateTimeOffset | Ngày bắt đầu lọc |
| `ToDate` | DateTimeOffset | Ngày kết thúc lọc |

### Error Response (400 Bad Request)

```json
{
  "Message": "Không xác định được người dùng"
}
```

### Error Response (401 Unauthorized)

```json
{
  "Message": "Unauthorized"
}
```

### Error Response (403 Forbidden)

```json
{
  "Message": "You do not have permission to access this resource"
}
```

## Business Logic

### 1. Định Nghĩa Tuyến

**Tuyến** = Địa chỉ gửi - Địa chỉ giao

- Địa chỉ gửi: `OrderAddress` có `AddressType = 1` (Source)
- Địa chỉ giao: `OrderAddress` có `AddressType = 2` (Dest)
- Tuyến được xác định dựa trên **CityId** (Level = 1 trong bảng Location)

### 2. Tính Toán Doanh Số

```
Revenue = Fee + CodFee + PickupSubFee + DeliverySubFee - Discount
```

Trong đó:
- `Fee`: Cước giao hàng chính
- `CodFee`: Phí thu hộ (COD)
- `PickupSubFee`: Phụ phí lấy hàng
- `DeliverySubFee`: Phụ phí giao hàng
- `Discount`: Giảm giá

### 3. Phân Loại Trạng Thái

| # | Trạng thái | Status | PhysicalStatus | Mô tả |
|---|------------|--------|----------------|-------|
| 1 | Chờ lấy hàng | 1 | 1 | Đơn hàng mới tạo, chờ lái xe đến lấy |
| 2 | Đang lấy hàng | 1 | 12 | Lái xe đang trên đường đến lấy hàng |
| 3 | Đã lấy hàng | 3 | 13 | Đã lấy hàng thành công |
| 4 | Trung chuyển | 3 | 20, 30 | Đang trong quá trình vận chuyển giữa các chi nhánh |
| 5 | Đang giao hàng | 3 | 53 | Lái xe đang trên đường giao hàng |
| 6 | Đã giao hàng | 55 | 55 | Giao hàng thành công |

### 4. Điều Kiện Lọc

#### Luôn áp dụng:
- ✅ Chỉ lấy đơn có đủ địa chỉ gửi và địa chỉ giao
- ✅ Loại trừ đơn đã xóa (`IsDeleted = 0`)
- ✅ Loại trừ đơn hủy (`Status != 8`)

#### Theo role:
- **Founder**: Xem tất cả hoặc lọc theo `SaleId` tùy chọn
- **User thường**: Tự động lọc theo `UserId` của chính mình

## Testing

### cURL Example

```bash
curl -X POST "https://localhost:7284/api/Report/OrderReportByRoute" \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "Pagination": {
      "PageIndex": 1,
      "PageSize": 100
    },
    "Sort": {
      "Predicate": "",
      "Reverse": false
    },
    "Search": {
      "PredicateObject": {
        "StartDate": "2025-01-01T00:00:00+07:00",
        "EndDate": "2025-01-31T23:59:59+07:00",
        "SourceCityIds": [1],
        "DestCityIds": [79]
      }
    }
  }'
```

### Postman Collection

**Request:**
```
Method: POST
URL: {{baseUrl}}/api/Report/OrderReportByRoute
Headers:
  Authorization: Bearer {{token}}
  Content-Type: application/json
Body (raw JSON):
{
  "Pagination": {
    "PageIndex": 1,
    "PageSize": 100
  },
  "Sort": {
    "Predicate": "",
    "Reverse": false
  },
  "Search": {
    "PredicateObject": {
      "StartDate": "2025-01-01T00:00:00+07:00",
      "EndDate": "2025-01-31T23:59:59+07:00"
    }
  }
}
```

### Swagger UI

Truy cập: `https://localhost:7284/swagger/index.html`

1. Tìm endpoint: `POST /api/Report/OrderReportByRoute`
2. Click "Try it out"
3. Nhập JWT token vào ô "Authorization"
4. Nhập request body
5. Click "Execute"

## Performance Considerations

### Database Indexes (Khuyến nghị)

Để tối ưu performance, nên tạo các index sau:

```sql
-- Order table
CREATE INDEX IX_Order_CreatedTime ON [Order](CreatedTime) WHERE IsDeleted = 0;
CREATE INDEX IX_Order_SaleId ON [Order](SaleId) WHERE IsDeleted = 0;

-- OrderAddress table
CREATE INDEX IX_OrderAddress_OrderId_AddressType ON OrderAddress(OrderId, AddressType);
CREATE INDEX IX_OrderAddress_CityId ON OrderAddress(CityId);

-- Location table
CREATE INDEX IX_Location_Id_Level ON Location(Id, Level);

-- OrderOps table
CREATE INDEX IX_OrderOps_Status_PhysicalStatus ON OrderOps([Status], PhysicalStatus);

-- OrderFin table
CREATE NONCLUSTERED INDEX IX_OrderFin_Id ON OrderFin(Id);
```

### Expected Response Time

| Số lượng đơn | Thời gian dự kiến |
|--------------|-------------------|
| < 1,000 | < 500ms |
| 1,000 - 10,000 | 500ms - 2s |
| > 10,000 | 2s - 5s |

## Common Use Cases

### 1. Dashboard Báo Cáo Theo Tuyến

Hiển thị tổng quan doanh số và số lượng đơn hàng theo từng tuyến vận chuyển.

### 2. Phân Tích Hiệu Suất Tuyến

Xác định các tuyến có lượng đơn hàng nhiều nhất, doanh số cao nhất.

### 3. Theo Dõi Trạng Thái Đơn Hàng

Giám sát tỷ lệ đơn hàng ở từng trạng thái theo từng tuyến.

### 4. Báo Cáo Theo Sale

Founder có thể xem báo cáo theo từng Sale để đánh giá hiệu suất.

### 5. Dự Báo & Lập Kế Hoạch

Dựa vào dữ liệu lịch sử để dự đoán xu hướng và phân bổ tài nguyên.

## Error Handling

### Common Errors

| Error | Nguyên nhân | Giải pháp |
|-------|-------------|-----------|
| 401 Unauthorized | Không có JWT token hoặc token hết hạn | Đăng nhập lại để lấy token mới |
| 403 Forbidden | User không có permission `Report.OrderReportByRoute` | Liên hệ admin để cấp quyền |
| 400 Bad Request | Tham số không hợp lệ | Kiểm tra lại format của StartDate/EndDate |
| 500 Internal Server Error | Lỗi server hoặc stored procedure | Kiểm tra log, đảm bảo stored procedure đã được tạo |

## Related Documentation

- [Stored Procedure Documentation](../docs/Reports/OrderReportByRoute.md)
- [Test Scripts](../Scripts/Test_sp_GetOrderReportByRoute.sql)
- [Order Status Enums](../../Lada.Framework/Data/Enums/OrderEnums.cs)

## Changelog

| Ngày | Phiên bản | Mô tả |
|------|-----------|-------|
| 2025-01-06 | 1.2 | Hỗ trợ lọc theo nhiều tỉnh/thành phố cùng lúc - Multiple select cho SourceCityIds và DestCityIds |
| 2025-01-06 | 1.1 | Cập nhật request structure theo SmartTableParam với Pagination và Sort |
| 2025-01-06 | 1.0 | Phiên bản đầu tiên - API báo cáo đơn hàng theo tuyến |

## Support & Contact

Nếu có câu hỏi hoặc phát hiện lỗi, vui lòng liên hệ team phát triển.

