# Agrawal Properties — API Documentation

**Base URL:** `/api`  
**Version:** 1.0  
**Auth:** Laravel Sanctum — Bearer token (except Login)

---

## Response Envelope

Every API response (success or failure) is wrapped in a consistent envelope.

### Success
```json
{
    "status": true,
    "message": "Human-readable success message.",
    "result": { ... }
}
```

### Success (Paginated List)
```json
{
    "status": true,
    "message": "Human-readable success message.",
    "result": {
        "data": [ ... ],
        "pagination": {
            "total": 100,
            "per_page": 20,
            "current_page": 1,
            "last_page": 5,
            "from": 1,
            "to": 20
        }
    }
}
```

### Error
```json
{
    "status": false,
    "message": "Human-readable error message."
}
```

### Validation Error
```json
{
    "status": false,
    "message": "Validation failed.",
    "errors": {
        "email": ["The email field is required."],
        "password": ["The password field is required."]
    }
}
```

---

## HTTP Status Codes

| Code | Meaning |
|------|---------|
| `200` | Success |
| `201` | Resource created |
| `401` | Unauthenticated (missing or invalid token) |
| `403` | Unauthorized (insufficient permissions) |
| `404` | Resource not found |
| `422` | Validation failed |
| `500` | Internal server error |

---

## Common Headers

```
Content-Type: application/json
Accept: application/json
Authorization: Bearer {token}
```

> `Authorization` is required on all endpoints except `POST /api/auth/login`.

---

## Authentication

### 1. Login

| | |
|---|---|
| **Method** | `POST` |
| **Endpoint** | `/api/auth/login` |
| **Auth** | None (public) |

**Request Body**

| Field | Type | Required | Rules |
|-------|------|----------|-------|
| `email` | string | Yes | Valid email |
| `password` | string | Yes | — |

**Success Response** `200`
```json
{
    "status": true,
    "message": "Login successful.",
    "result": {
        "token": "1|abc123...",
        "role": "admin",
        "user": {
            "id": 1,
            "name": "John Doe",
            "email": "john@example.com"
        }
    }
}
```

**Error Responses**

| HTTP | `message` |
|------|-----------|
| `401` | `Invalid email or password.` |
| `403` | `Your account has been deactivated. Please contact your administrator.` |
| `422` | `Validation failed.` (+ `errors`) |

---

### 2. Logout

| | |
|---|---|
| **Method** | `POST` |
| **Endpoint** | `/api/auth/logout` |
| **Auth** | Bearer token |

Revokes the current access token.

**Success Response** `200`
```json
{
    "status": true,
    "message": "Logged out successfully."
}
```

---

### 3. Get Authenticated User

| | |
|---|---|
| **Method** | `GET` |
| **Endpoint** | `/api/auth/me` |
| **Auth** | Bearer token |

**Success Response** `200`
```json
{
    "status": true,
    "message": "User profile fetched successfully.",
    "result": {
        "id": 1,
        "name": "John Doe",
        "email": "john@example.com",
        "role": "admin",
        "mobile": "9827700000"
    }
}
```

---

## Dashboard

### 4. Get Dashboard Summary

| | |
|---|---|
| **Method** | `GET` |
| **Endpoint** | `/api/dashboard` |
| **Auth** | Bearer token |
| **Roles** | Admin, Staff |

Returns a summary of key counts and today's reminders. Data is automatically scoped based on the authenticated user's role:

| Field | Admin (global) | Staff (own) |
|---|---|---|
| `assigned_clients_count` | Total clients in the system | Clients assigned to this user |
| `open_leads_count` | All active (non-closed) leads | Active leads assigned to this user |
| `todays_reminders_count` | All pending reminders due today | Pending reminders due today assigned to this user |
| `upcoming_visits_count` | All site visits on or after today | Site visits for this user's assigned leads |
| `todays_reminders` | All today's pending reminders | Today's pending reminders assigned to this user |

**Success Response** `200`
```json
{
    "status": true,
    "message": "Dashboard data fetched successfully.",
    "result": {
        "assigned_clients_count": 12,
        "open_leads_count": 5,
        "todays_reminders_count": 3,
        "upcoming_visits_count": 2,
        "todays_reminders": [
            {
                "id": 1,
                "message": "Follow up with client about financing",
                "due_at": "2026-05-22 11:00:00",
                "lead_id": 25,
                "client_name": "John Doe",
                "requirement_summary": "2 BHK Apartment in Shastri Nagar"
            }
        ]
    }
}
```

**Reminder item fields**

| Field | Type | Description |
|---|---|---|
| `id` | integer | Reminder ID |
| `message` | string | Reminder message |
| `due_at` | datetime | Due date and time (`Y-m-d H:i:s`) |
| `lead_id` | integer | Associated lead ID |
| `client_name` | string\|null | Client's full name |
| `requirement_summary` | string\|null | Requirement title linked to the lead |

---

## Owners

### 5. List Owners

| | |
|---|---|
| **Method** | `GET` |
| **Endpoint** | `/api/owners` |
| **Auth** | Bearer token |
| **Roles** | Admin, Staff |

**Query Parameters**

| Param | Type | Description |
|-------|------|-------------|
| `search` | string | Filter by name or mobile |
| `page` | integer | Page number (default: 1) |

**Success Response** `200`
```json
{
    "status": true,
    "message": "Owners fetched successfully.",
    "result": {
        "data": [
            {
                "id": 1,
                "name": "Ramesh Sharma",
                "mobile": "9827700001",
                "email": "ramesh@example.com",
                "city": "Indore",
                "properties_count": 3
            }
        ],
        "pagination": {
            "total": 50,
            "per_page": 20,
            "current_page": 1,
            "last_page": 3,
            "from": 1,
            "to": 20
        }
    }
}
```

---

### 6. Get Owner

| | |
|---|---|
| **Method** | `GET` |
| **Endpoint** | `/api/owners/{id}` |
| **Auth** | Bearer token |
| **Roles** | Admin, Staff |

**Success Response** `200`
```json
{
    "status": true,
    "message": "Owner fetched successfully.",
    "result": {
        "id": 1,
        "name": "Ramesh Sharma",
        "mobile": "9827700001",
        "email": "ramesh@example.com",
        "city": "Indore",
        "properties_count": 3
    }
}
```

**Error Responses**

| HTTP | `message` |
|------|-----------|
| `404` | `The requested resource was not found.` |

---

## Settings

### 7. List Property Types

| | |
|---|---|
| **Method** | `GET` |
| **Endpoint** | `/api/property-types` |
| **Auth** | Bearer token |
| **Roles** | Admin, Staff |

Returns all active property types ordered by `sort_order`. Used by mobile apps to build dynamic property creation forms.

**Success Response** `200`
```json
{
    "status": true,
    "message": "Property types fetched successfully.",
    "result": [
        { "id": 1, "name": "Flat / Apartment", "slug": "flat",      "sort_order": 1 },
        { "id": 2, "name": "Plot / Land",       "slug": "plot",      "sort_order": 2 },
        { "id": 3, "name": "Villa",             "slug": "villa",     "sort_order": 3 },
        { "id": 4, "name": "Commercial",        "slug": "commercial","sort_order": 4 }
    ]
}
```

**Response fields**

| Field | Type | Description |
|---|---|---|
| `id` | integer | Property type ID |
| `name` | string | Display name |
| `slug` | string | Machine-readable identifier (used as the `type` filter on `/api/properties`) |
| `sort_order` | integer | Display order in forms |

---

### 8. List Sourcing Channels

| | |
|---|---|
| **Method** | `GET` |
| **Endpoint** | `/api/sourcing-channels` |
| **Auth** | Bearer token |
| **Roles** | Admin, Staff |

Returns all active sourcing channels ordered by name. Used by mobile apps to populate sourcing channel dropdowns when creating properties or clients.

**Success Response** `200`
```json
{
    "status": true,
    "message": "Sourcing channels fetched successfully.",
    "result": [
        { "id": 1, "name": "Direct Inquiry" },
        { "id": 2, "name": "Referral" },
        { "id": 3, "name": "Walk-In" }
    ]
}
```

**Response fields**

| Field | Type | Description |
|---|---|---|
| `id` | integer | Sourcing channel ID |
| `name` | string | Display name |

---

## Properties

### 9. List Properties

| | |
|---|---|
| **Method** | `GET` |
| **Endpoint** | `/api/properties` |
| **Auth** | Bearer token |
| **Roles** | Admin (all statuses), Staff (published only) |

**Query Parameters**

| Param | Type | Description |
|-------|------|-------------|
| `page` | integer | Page number (default: 1) |
| `search` | string | Search by title, location, or city |
| `type` | string | Filter by property type slug (e.g. `flat`, `plot`) |
| `listing_purpose` | string | `for_sale` or `for_rent` |
| `area` | string | Filter by location or city (partial match) |
| `min_price` | numeric | Minimum price in INR (excludes price-on-request listings) |
| `max_price` | numeric | Maximum price in INR (excludes price-on-request listings) |
| `min_size` | numeric | Minimum area in sq ft |
| `max_size` | numeric | Maximum area in sq ft |

**Success Response** `200`
```json
{
    "status": true,
    "message": "Properties fetched successfully.",
    "result": {
        "data": [
            {
                "id": 1,
                "title": "3BHK Flat in Vijay Nagar",
                "type": "flat",
                "listing_purpose": "for_sale",
                "status": "published",
                "city": "Indore",
                "area": "1200.00",
                "area_unit": "sqft",
                "price": "5500000.00",
                "formatted_price": "₹ 55,00,000"
            }
        ],
        "pagination": { "total": 30, "per_page": 20, "current_page": 1, "last_page": 2, "from": 1, "to": 20 }
    }
}
```

---

### 10. Nearby Properties

| | |
|---|---|
| **Endpoint** | `GET /api/properties/nearby` |
| **Auth** | Bearer token required |
| **Access** | All authenticated users |

Returns published properties within the given radius of a coordinate point, ordered by distance ascending. Maximum 50 results.

**Query Parameters**

| Parameter | Type | Required | Description |
|---|---|---|---|
| `lat` | float | Yes | Latitude of centre point (-90 to 90) |
| `lng` | float | Yes | Longitude of centre point (-180 to 180) |
| `radius_km` | float | Yes | Search radius in kilometres (0.1 – 100) |

**Example Request**
```
GET /api/properties/nearby?lat=23.0225&lng=72.5714&radius_km=10
Authorization: Bearer {token}
```

**Example Response**
```json
{
    "status": true,
    "message": "Nearby properties fetched successfully.",
    "result": [
        {
            "id": 12,
            "title": "2BHK Near Satellite",
            "type": "apartment",
            "type_label": "Apartment",
            "listing_purpose": "sell",
            "listing_purpose_label": "For Sale",
            "location": "Satellite Road",
            "city": "Ahmedabad",
            "latitude": 23.0189,
            "longitude": 72.5680,
            "distance_km": 0.47,
            "area": "1200.00",
            "area_unit": "sq_ft",
            "price": 4500000,
            "price_on_request": false,
            "formatted_price": "₹45 Lakh",
            "cover_image": {
                "id": 88,
                "url": "https://example.com/api/media/images/88"
            }
        }
    ]
}
```

**Validation Errors (422)**
```json
{
    "message": "The given data was invalid.",
    "errors": {
        "lat": ["The lat field is required."],
        "radius_km": ["The radius km must not be greater than 100."]
    }
}
```

---

### 11. Get Property

| | |
|---|---|
| **Method** | `GET` |
| **Endpoint** | `/api/properties/{id}` |
| **Auth** | Bearer token |
| **Roles** | Admin (all statuses), Staff (published only) |

**Success Response** `200`
```json
{
    "status": true,
    "message": "Property fetched successfully.",
    "result": {
        "id": 1,
        "title": "3BHK Flat in Vijay Nagar",
        "listing_purpose": "for_sale",
        "listing_purpose_label": "For Sale",
        "type": "flat",
        "type_label": "Flat / Apartment",
        "status": "published",
        "location": "Vijay Nagar, Indore",
        "city": "Indore",
        "latitude": 22.7196,
        "longitude": 75.8577,
        "area": "1200.00",
        "area_unit": "sqft",
        "area_formatted": "1200.00 SQFT",
        "price": "4500000.00",
        "price_on_request": false,
        "formatted_price": "₹45,00,000",
        "description": "Spacious 3BHK flat with modern amenities.",
        "type_details": { "bedrooms": "3", "parking_available": "1" },
        "is_managed": false,
        "owner": { "id": 1, "name": "Ramesh Sharma", "mobile": "9827700001" },
        "sourcing_channel": { "id": 2, "name": "Referral" },
        "broker": null,
        "tags": ["Ready to Move", "Corner Unit"],
        "cover_image": { "id": 3, "url": "https://domain.com/api/media/images/3" },
        "images": [
            { "id": 3, "url": "https://domain.com/api/media/images/3", "sort_order": 0, "mime_type": "image/jpeg", "size_bytes": 204800 }
        ],
        "documents": [
            { "id": 1, "document_type": "Sale Deed", "original_name": "deed.pdf", "mime_type": "application/pdf", "size_bytes": 215040, "download_url": "https://domain.com/api/property-documents/1/download", "uploaded_at": "2026-05-18T09:00:00.000000Z" }
        ],
        "images_count": 1,
        "documents_count": 1,
        "price_history": [
            { "old_price": "4200000.00", "new_price": "4500000.00", "changed_by_name": "Admin", "changed_at": "2026-05-20T10:00:00.000000Z", "note": "Market adjustment" }
        ],
        "created_at": "2026-05-10T09:00:00.000000Z",
        "updated_at": "2026-05-20T10:00:00.000000Z"
    }
}
```

> **Notes:**
> - `tags` returns only **active** tags (deactivated tags are excluded).
> - `is_managed` is `true` only for `for_rent` properties managed by Agrawal Properties for rent collection tracking. Always `false` for `for_sale` properties.
> - `type_details` is a free-form object containing the custom field values configured for that property type. Field keys and structures vary by type.

**Error Responses**

| HTTP | `message` |
|------|-----------|
| `404` | `The requested property was not found.` |

---

### 12. Get Property Images

| | |
|---|---|
| **Method** | `GET` |
| **Endpoint** | `/api/properties/{id}/images` |
| **Auth** | Bearer token |
| **Roles** | Admin, Staff |

**Success Response** `200`
```json
{
    "status": true,
    "message": "Property images fetched successfully.",
    "result": [
        {
            "id": 1,
            "url": "https://domain.com/api/media/images/1",
            "sort_order": 0
        }
    ]
}
```

---

### 13. Upload Property Image

| | |
|---|---|
| **Method** | `POST` |
| **Endpoint** | `/api/properties/{id}/images` |
| **Auth** | Bearer token |
| **Roles** | Admin |
| **Content-Type** | `multipart/form-data` |

**Request Body**

| Field | Type | Required | Rules |
|-------|------|----------|-------|
| `image` | file | Yes | JPEG, PNG, WebP — max 10 MB |

**Success Response** `201`
```json
{
    "status": true,
    "message": "Image uploaded successfully.",
    "result": {
        "id": 5,
        "url": "https://domain.com/api/media/images/5",
        "sort_order": 4
    }
}
```

---

### 14. Get Property Documents

| | |
|---|---|
| **Method** | `GET` |
| **Endpoint** | `/api/properties/{id}/documents` |
| **Auth** | Bearer token |
| **Roles** | Admin, Staff |

**Success Response** `200`
```json
{
    "status": true,
    "message": "Property documents fetched successfully.",
    "result": [
        {
            "id": 1,
            "document_type": "Sale Deed",
            "original_name": "sale-deed.pdf",
            "size": 204800,
            "download_url": "https://domain.com/api/documents/1/download",
            "created_at": "2026-05-19T10:00:00.000000Z"
        }
    ]
}
```

---

## Clients

### 15. List Clients

| | |
|---|---|
| **Method** | `GET` |
| **Endpoint** | `/api/clients` |
| **Auth** | Bearer token |
| **Roles** | Admin (all), Staff (assigned only) |

**Query Parameters**

| Param | Type | Description |
|-------|------|-------------|
| `search` | string | Filter by name or mobile |
| `page` | integer | Page number (default: 1) |

**Success Response** `200`
```json
{
    "status": true,
    "message": "Clients fetched successfully.",
    "result": {
        "data": [
            {
                "id": 1,
                "first_name": "Ankit",
                "last_name": "Patel",
                "full_name": "Ankit Patel",
                "mobile": "9827700002",
                "email": "ankit@example.com",
                "interest_type": "buy",
                "city": "Indore"
            }
        ],
        "pagination": { "total": 40, "per_page": 20, "current_page": 1, "last_page": 2, "from": 1, "to": 20 }
    }
}
```

---

### 16. Get Client

| | |
|---|---|
| **Method** | `GET` |
| **Endpoint** | `/api/clients/{id}` |
| **Auth** | Bearer token |
| **Roles** | Admin, Assigned Staff |

**Success Response** `200`
```json
{
    "status": true,
    "message": "Client fetched successfully.",
    "result": {
        "id": 1,
        "first_name": "Ankit",
        "last_name": "Patel",
        "full_name": "Ankit Patel",
        "mobile": "9827700002",
        "email": "ankit@example.com",
        "address": "123 Main Street",
        "city": "Indore",
        "interest_type": "buy",
        "sourcing_channel": { "id": 1, "name": "Walk-In" },
        "assigned_to": { "id": 2, "name": "Staff Member" }
    }
}
```

**Error Responses**

| HTTP | `message` |
|------|-----------|
| `403` | `You are not authorized to view this client.` |
| `404` | `The requested resource was not found.` |

---

### 17. Create Client

| | |
|---|---|
| **Method** | `POST` |
| **Endpoint** | `/api/clients` |
| **Auth** | Bearer token |
| **Roles** | Admin, Staff |

**Request Body**

| Field | Type | Required | Rules |
|-------|------|----------|-------|
| `first_name` | string | Yes | Max 100 chars |
| `last_name` | string | Yes | Max 100 chars |
| `mobile` | string | Yes | Exactly 10 digits |
| `email` | string | No | Valid email |
| `address` | string | No | — |
| `city` | string | No | Max 100 chars |
| `sourcing_channel_id` | integer | Yes | Must exist in `sourcing_channels` |
| `interest_type` | string | Yes | `buy`, `rent`, or `both` |
| `assigned_to` | integer | No | Admin only; staff is auto-assigned to self |

**Success Response** `201`
```json
{
    "status": true,
    "message": "Client created successfully.",
    "result": { ... }
}
```

---

### 18. Update Client

| | |
|---|---|
| **Method** | `PUT` |
| **Endpoint** | `/api/clients/{id}` |
| **Auth** | Bearer token |
| **Roles** | Admin, Assigned Staff |

Request body mirrors Create Client (without `assigned_to`).

**Success Response** `200`
```json
{
    "status": true,
    "message": "Client updated successfully.",
    "result": { ... }
}
```

**Error Responses**

| HTTP | `message` |
|------|-----------|
| `403` | `You are not authorized to update this client.` |

---

### 19. Clients with Requirements

| | |
|---|---|
| **Method** | `GET` |
| **Endpoint** | `/api/clients-with-requirements` |
| **Auth** | Bearer token |
| **Roles** | Admin, Staff |

Returns a paginated list of clients, each with their full list of requirements embedded. Useful for mobile screens that need to display a client's search requirements without making separate requests.

| Role | Clients returned | Requirements returned |
|---|---|---|
| Admin | All clients | All requirements per client |
| Staff | Assigned clients only | All requirements for each assigned client |

**Query Parameters**

| Param | Type | Description |
|-------|------|-------------|
| `page` | integer | Page number (default: 1) |
| `search` | string | Filter clients by name or mobile |

**Success Response** `200`
```json
{
    "status": true,
    "message": "Clients with requirements fetched successfully.",
    "result": {
        "data": [
            {
                "id": 1,
                "full_name": "Ravi Mehta",
                "mobile": "9876543210",
                "email": "ravi@example.com",
                "city": "Indore",
                "interest_type": "buy",
                "assigned_to": { "id": 2, "name": "Staff Name" },
                "requirements_count": 2,
                "active_requirements_count": 1,
                "requirements": [
                    {
                        "id": 1,
                        "title": "Buy - Flat / Apartment - Vijay Nagar",
                        "status": "active",
                        "interest_type": "buy",
                        "interest_type_label": "Buy",
                        "property_type": "flat",
                        "property_type_label": "Flat / Apartment",
                        "min_budget": 5000000,
                        "max_budget": 10000000,
                        "min_size": 1000,
                        "max_size": 2000,
                        "preferred_location": "Vijay Nagar",
                        "notes": "Prefer north-facing flat",
                        "created_at": "2026-05-01T10:00:00Z"
                    }
                ],
                "created_at": "2026-04-15T09:00:00Z"
            }
        ],
        "pagination": { "total": 45, "per_page": 20, "current_page": 1, "last_page": 3, "from": 1, "to": 20 }
    }
}
```

**Requirement item fields**

| Field | Type | Description |
|---|---|---|
| `id` | integer | Requirement ID |
| `title` | string | Auto-generated summary title |
| `status` | string | `active` or `closed` |
| `interest_type` | string | `buy` or `rent` |
| `interest_type_label` | string | Human-readable label |
| `property_type` | string | e.g. `flat`, `plot`, `agricultural_land` |
| `property_type_label` | string | Human-readable label |
| `min_budget` | number\|null | Minimum budget in INR |
| `max_budget` | number\|null | Maximum budget in INR |
| `min_size` | number\|null | Minimum size in sq ft |
| `max_size` | number\|null | Maximum size in sq ft |
| `preferred_location` | string\|null | Preferred area / locality |
| `notes` | string\|null | Additional notes |
| `created_at` | datetime | When the requirement was created |

---

## Client Requirements

### 20. List Requirements

| | |
|---|---|
| **Method** | `GET` |
| **Endpoint** | `/api/clients/{id}/requirements` |
| **Auth** | Bearer token |
| **Roles** | Admin, Assigned Staff |

**Success Response** `200`
```json
{
    "status": true,
    "message": "Requirements fetched successfully.",
    "result": [
        {
            "id": 1,
            "title": "Buy - Flat - Vijay Nagar",
            "interest_type": "buy",
            "property_type": "flat",
            "min_budget": 3000000,
            "max_budget": 5000000,
            "min_size": null,
            "max_size": null,
            "preferred_location": "Vijay Nagar",
            "notes": null,
            "status": "active",
            "lead_stage": "options_suggested",
            "lead_stage_label": "Options Suggested",
            "created_at": "2026-05-19T10:00:00.000000Z"
        }
    ]
}
```

**Error Responses**

| HTTP | `message` |
|------|-----------|
| `403` | `You are not authorized to view this client's requirements.` |

---

### 21. Add Requirement

| | |
|---|---|
| **Method** | `POST` |
| **Endpoint** | `/api/clients/{id}/requirements` |
| **Auth** | Bearer token |
| **Roles** | Admin, Assigned Staff |

**Request Body**

| Field | Type | Required | Rules |
|-------|------|----------|-------|
| `interest_type` | string | Yes | `buy` or `rent` |
| `property_type` | string | No | `any`, `plot`, `flat`, `bungalow`, `agricultural_land`, `commercial` |
| `min_budget` | numeric | No | ≤ `max_budget` if both provided |
| `max_budget` | numeric | No | — |
| `min_size` | numeric | No | ≤ `max_size` if both provided |
| `max_size` | numeric | No | — |
| `preferred_location` | string | No | Max 255 chars |
| `notes` | string | No | — |

**Success Response** `201`
```json
{
    "status": true,
    "message": "Requirement created successfully.",
    "result": { ... }
}
```

---

### 22. Close Requirement

| | |
|---|---|
| **Method** | `PUT` |
| **Endpoint** | `/api/requirements/{id}/close` |
| **Auth** | Bearer token |
| **Roles** | Admin, Assigned Staff |

**Request Body**

| Field | Type | Required | Rules |
|-------|------|----------|-------|
| `close_reason` | string | Yes | `won` or `lost` |
| `notes` | string | No | Max 500 chars |

**Success Response** `200`
```json
{
    "status": true,
    "message": "Requirement closed successfully.",
    "result": {
        "id": 1,
        "status": "closed",
        "lead_stage": "closed_won",
        "lead_stage_label": "Closed — Won"
    }
}
```

**Error Responses**

| HTTP | `message` |
|------|-----------|
| `403` | `You are not authorized to close this requirement.` |
| `422` | `This requirement is already closed.` |

---

## Pipeline Stages

### 23. List Pipeline Stages

| | |
|---|---|
| **Method** | `GET` |
| **Endpoint** | `/api/stages` |
| **Auth** | Bearer token |
| **Roles** | Admin, Staff |

Returns all active lead pipeline stages in display order. Use the returned `id` values when calling **Update Lead Stage**.

**Success Response** `200`
```json
{
    "status": true,
    "message": "Stages fetched successfully.",
    "result": [
        {
            "id": 1,
            "name": "New Lead",
            "color": "#6c757d",
            "order": 1,
            "is_default": true,
            "is_closed_won": false,
            "is_closed_lost": false
        },
        {
            "id": 6,
            "name": "Closed — Won",
            "color": "#198754",
            "order": 6,
            "is_default": false,
            "is_closed_won": true,
            "is_closed_lost": false
        }
    ]
}
```

---

## Leads

### 24. List Leads

| | |
|---|---|
| **Method** | `GET` |
| **Endpoint** | `/api/leads` |
| **Auth** | Bearer token |
| **Roles** | Admin (all active leads), Staff (assigned only) |

Returns paginated active leads ordered by most recent activity.

**Success Response** `200`
```json
{
    "status": true,
    "message": "Leads fetched successfully.",
    "result": {
        "data": [
            {
                "id": 1,
                "client_id": 5,
                "client_name": "Ankit Patel",
                "requirement_summary": "Buy - Flat - Vijay Nagar",
                "current_stage": {
                    "id": 3,
                    "name": "Options Suggested",
                    "color": "#0d6efd"
                },
                "days_in_stage": 4,
                "assigned_to": "Rahul Sharma",
                "last_activity_at": "2026-05-19T14:30:00.000000Z",
                "created_at": "2026-05-10T09:00:00.000000Z"
            }
        ],
        "pagination": {
            "total": 45,
            "per_page": 20,
            "current_page": 1,
            "last_page": 3
        }
    }
}
```

---

### 25. Get Lead

| | |
|---|---|
| **Method** | `GET` |
| **Endpoint** | `/api/leads/{id}` |
| **Auth** | Bearer token |
| **Roles** | Admin, Assigned Staff |

**Success Response** `200`
```json
{
    "status": true,
    "message": "Lead fetched successfully.",
    "result": {
        "id": 1,
        "client_name": "Ankit Patel",
        "requirement_summary": "Buy - Flat - Vijay Nagar",
        "current_stage": { "id": 3, "name": "Options Suggested", "color": "#0d6efd" },
        "days_in_stage": 4,
        "assigned_to": "Rahul Sharma",
        "last_activity_at": "2026-05-19T14:30:00.000000Z",
        "created_at": "2026-05-10T09:00:00.000000Z",
        "requirement": {
            "id": 1,
            "title": "Buy - Flat - Vijay Nagar",
            "interest_type": "buy",
            "property_type": "flat",
            "budget": "₹30L – ₹50L",
            "size": null,
            "preferred_location": "Vijay Nagar",
            "notes": null,
            "status": "active"
        },
        "stage_history": [
            {
                "from_stage": null,
                "to_stage": "New Lead",
                "changed_by": "Admin",
                "note": null,
                "created_at": "2026-05-10T09:00:00.000000Z"
            },
            {
                "from_stage": "New Lead",
                "to_stage": "Site Visit Scheduled",
                "changed_by": "Rahul Sharma",
                "note": "Client keen on 3BHK options",
                "created_at": "2026-05-15T11:00:00.000000Z"
            }
        ],
        "shortlisted_properties": [
            {
                "shortlist_id": 3,
                "property_id": 7,
                "title": "3BHK Flat in Vijay Nagar",
                "type": "Flat / Apartment",
                "listing_purpose": "For Sale",
                "location": "Vijay Nagar, Indore",
                "city": "Indore",
                "price": "₹45,00,000",
                "area": "1200.00 sqft",
                "cover_image": "https://domain.com/storage/images/property/abc.jpg",
                "shortlisted_by": "Rahul Sharma",
                "shortlisted_at": "2026-05-18T11:00:00.000000Z"
            }
        ],
        "site_visits": [
            {
                "id": 5,
                "property_id": 7,
                "property_title": "3BHK Flat in Vijay Nagar",
                "visit_date": "2026-05-16",
                "visit_time": "11:00",
                "accompanying_staff": "Rahul Sharma",
                "notes": "Client liked the layout.",
                "stage_at_visit": "Site Visit Scheduled",
                "logged_by": "Rahul Sharma",
                "logged_at": "2026-05-16T12:00:00.000000Z"
            }
        ],
        "activity_log": [
            {
                "id": 12,
                "action": "site_visit_logged",
                "description": "Site visit logged for 3BHK Flat in Vijay Nagar on 2026-05-16.",
                "performed_by": "Rahul Sharma",
                "created_at": "2026-05-16T12:00:00.000000Z"
            },
            {
                "id": 11,
                "action": "shortlist_added",
                "description": "Property shortlisted: 3BHK Flat in Vijay Nagar.",
                "performed_by": "Rahul Sharma",
                "created_at": "2026-05-15T14:00:00.000000Z"
            }
        ],
        "quote_history": [
            {
                "id": 2,
                "property_id": 7,
                "property_title": "3BHK Flat in Vijay Nagar",
                "quoted_price": 4300000,
                "quote_date": "2026-05-17",
                "shared_by": "Rahul Sharma",
                "notes": "Negotiated down from 45L.",
                "created_by": "Rahul Sharma",
                "created_at": "2026-05-17T15:00:00.000000Z"
            }
        ],
        "agreement": null
    }
}
```

> **Notes:**
> - `shortlisted_properties`, `site_visits`, `activity_log`, and `quote_history` are always present and reflect the current database state (never hardcoded empty).
> - `agreement` is `null` until a final deal is recorded via `POST /api/leads/{id}/agreement`. Once recorded, it contains the full agreement object (see §34). For rental deals, additional fields (`agreement_start_date`, `agreement_end_date`, `monthly_rent`, `rent_escalation_clause`, `expiry_reminder_days`) are included.
> - `activity_log` is ordered newest-first.

**Error Responses**

| HTTP | `message` |
|------|-----------|
| `403` | `You are not authorized to view this lead.` |
| `404` | `The requested resource was not found.` |

---

### 26. Update Lead Stage

| | |
|---|---|
| **Method** | `PUT` |
| **Endpoint** | `/api/leads/{id}/stage` |
| **Auth** | Bearer token |
| **Roles** | Admin, Assigned Staff |

Moving a lead to a closed stage (`is_closed_won` or `is_closed_lost`) automatically closes its linked requirement.

**Request Body**

| Field | Type | Required | Rules |
|-------|------|----------|-------|
| `stage_id` | integer | Yes | Must be an active stage ID |
| `note` | string | No | Max 500 chars |

**Success Response** `200`
```json
{
    "status": true,
    "message": "Lead stage updated successfully.",
    "result": {
        "id": 1,
        "client_name": "Ankit Patel",
        "requirement_summary": "Buy - Flat - Vijay Nagar",
        "current_stage": { "id": 4, "name": "Negotiation", "color": "#ffc107" },
        "days_in_stage": 0,
        "assigned_to": "Rahul Sharma",
        "last_activity_at": "2026-05-20T10:00:00.000000Z",
        "created_at": "2026-05-10T09:00:00.000000Z"
    }
}
```

**Error Responses**

| HTTP | `message` |
|------|-----------|
| `403` | `You are not authorized to update this lead.` |
| `422` | `The selected stage is already the current stage.` |
| `422` | `The selected stage is not available.` |

---

## Shortlisted Properties

### 27. List Shortlisted Properties

| | |
|---|---|
| **Method** | `GET` |
| **Endpoint** | `/api/leads/{id}/shortlisted` |
| **Auth** | Bearer token |
| **Roles** | Admin, Assigned Staff |

**Success Response** `200`
```json
{
    "status": true,
    "message": "Shortlisted properties fetched.",
    "result": {
        "shortlisted": [
            {
                "shortlist_id": 3,
                "property_id": 7,
                "title": "3BHK Flat in Vijay Nagar",
                "type": "Flat / Apartment",
                "listing_purpose": "For Sale",
                "location": "Vijay Nagar, Indore",
                "city": "Indore",
                "price": "₹45,00,000",
                "area": "1200 sqft",
                "cover_image": "https://domain.com/storage/images/abc.jpg",
                "shortlisted_by": "Rahul Sharma",
                "shortlisted_at": "2026-05-18T11:00:00.000000Z"
            }
        ],
        "count": 1
    }
}
```

---

### 28. Shortlist a Property

| | |
|---|---|
| **Method** | `POST` |
| **Endpoint** | `/api/leads/{id}/shortlist` |
| **Auth** | Bearer token |
| **Roles** | Admin, Assigned Staff |

**Request Body**

| Field | Type | Required | Rules |
|-------|------|----------|-------|
| `property_id` | integer | Yes | Must exist in `properties` |

**Success Response** `201`
```json
{
    "status": true,
    "message": "Property shortlisted successfully.",
    "result": { "shortlist_id": 3 }
}
```

**Error Responses**

| HTTP | `message` |
|------|-----------|
| `409` | `Property is already shortlisted for this lead.` |

---

### 29. Remove from Shortlist

| | |
|---|---|
| **Method** | `DELETE` |
| **Endpoint** | `/api/leads/{id}/shortlist/{property_id}` |
| **Auth** | Bearer token |
| **Roles** | Admin, Assigned Staff |

**Request Body**

| Field | Type | Required | Rules |
|-------|------|----------|-------|
| `reason` | string | Yes | — |
| `notes` | string | No | Max 500 chars |

**Success Response** `200`
```json
{
    "status": true,
    "message": "Property removed from shortlist.",
    "result": null
}
```

**Error Responses**

| HTTP | `message` |
|------|-----------|
| `404` | `Property is not shortlisted for this lead.` |

---

## Site Visits

### 30. List Site Visits

| | |
|---|---|
| **Method** | `GET` |
| **Endpoint** | `/api/leads/{id}/visits` |
| **Auth** | Bearer token |
| **Roles** | Admin, Assigned Staff |

**Success Response** `200`
```json
{
    "status": true,
    "message": "Site visits fetched.",
    "result": {
        "visits": [
            {
                "id": 1,
                "lead_id": 1,
                "property_id": 7,
                "property_title": "3BHK Flat in Vijay Nagar",
                "visit_date": "2026-05-15",
                "visit_time": "11:30",
                "accompanying_staff": "Rahul Sharma",
                "notes": "Client liked the floor plan.",
                "stage_at_visit": "Site Visit Done",
                "logged_by": "Rahul Sharma",
                "logged_at": "2026-05-15T12:00:00.000000Z"
            }
        ],
        "count": 1
    }
}
```

---

### 31. Log Site Visit

| | |
|---|---|
| **Method** | `POST` |
| **Endpoint** | `/api/leads/{id}/visits` |
| **Auth** | Bearer token |
| **Roles** | Admin, Assigned Staff |

**Request Body**

| Field | Type | Required | Rules |
|-------|------|----------|-------|
| `property_id` | integer | Yes | Must exist in `properties` |
| `visit_date` | date | Yes | Today or earlier (`YYYY-MM-DD`) |
| `visit_time` | string | Yes | `HH:MM` (24-hour) |
| `accompanying_staff` | string | Yes | Max 500 chars |
| `notes` | string | No | Max 2000 chars |

**Success Response** `201`
```json
{
    "status": true,
    "message": "Site visit logged successfully.",
    "result": {
        "id": 2,
        "lead_id": 1,
        "property_id": 7,
        "property_title": "3BHK Flat in Vijay Nagar",
        "visit_date": "2026-05-20",
        "visit_time": "10:00",
        "accompanying_staff": "Rahul Sharma",
        "notes": null,
        "stage_at_visit": "Site Visit Done",
        "logged_by": "Rahul Sharma",
        "logged_at": "2026-05-20T10:05:00.000000Z"
    }
}
```

---

## Activity Log

### 32. Get Lead Activity Log

| | |
|---|---|
| **Method** | `GET` |
| **Endpoint** | `/api/leads/{id}/activity` |
| **Auth** | Bearer token |
| **Roles** | Admin, Assigned Staff |

Returns the full chronological activity log for a lead (stage changes, notes, visits, quotes, etc.).

**Success Response** `200`
```json
{
    "status": true,
    "message": "Activity log fetched successfully.",
    "result": [
        {
            "id": 1,
            "action": "stage_changed",
            "description": "Stage changed to Options Suggested",
            "performed_by": "Rahul Sharma",
            "created_at": "2026-05-12T09:00:00.000000Z"
        },
        {
            "id": 2,
            "action": "note_added",
            "description": "Client interested in 3BHK options.",
            "performed_by": "Rahul Sharma",
            "created_at": "2026-05-14T15:30:00.000000Z"
        }
    ]
}
```

**Error Responses**

| HTTP | `message` |
|------|-----------|
| `403` | `You are not authorized to view this lead.` |

---

## Lead Notes

### 33. Add Note

| | |
|---|---|
| **Method** | `POST` |
| **Endpoint** | `/api/leads/{id}/notes` |
| **Auth** | Bearer token |
| **Roles** | Admin, Assigned Staff |

**Request Body**

| Field | Type | Required | Rules |
|-------|------|----------|-------|
| `note` | string | Yes | Max 2000 chars |

**Success Response** `201`
```json
{
    "status": true,
    "message": "Note added successfully.",
    "result": {
        "id": 5,
        "note": "Client confirmed budget up to 50L.",
        "created_by": "Rahul Sharma",
        "created_at": "2026-05-20T10:00:00.000000Z"
    }
}
```

**Error Responses**

| HTTP | `message` |
|------|-----------|
| `403` | `You are not authorized to add notes to this lead.` |

---

## Reminders

### 34. List Pending Reminders

| | |
|---|---|
| **Method** | `GET` |
| **Endpoint** | `/api/reminders` |
| **Auth** | Bearer token |
| **Roles** | Admin (all pending), Staff (assigned to self only) |

Returns all pending (not-done) reminders ordered by due date ascending.

**Success Response** `200`
```json
{
    "status": true,
    "message": "Reminders fetched successfully.",
    "result": [
        {
            "id": 3,
            "message": "Follow up with client about financing.",
            "remind_at": "2026-05-22T09:00:00.000000Z",
            "is_overdue": false,
            "lead_id": 1,
            "client_name": "Ankit Patel"
        }
    ]
}
```

---

### 35. Create Reminder

| | |
|---|---|
| **Method** | `POST` |
| **Endpoint** | `/api/leads/{id}/reminders` |
| **Auth** | Bearer token |
| **Roles** | Admin, Assigned Staff |

**Request Body**

| Field | Type | Required | Rules |
|-------|------|----------|-------|
| `message` | string | Yes | Max 500 chars |
| `remind_at` | date | Yes | Today or future (`YYYY-MM-DD`) |
| `assigned_to` | integer | No | Admin only — staff ID to assign to; defaults to lead's assigned staff |

**Success Response** `201`
```json
{
    "status": true,
    "message": "Reminder created successfully.",
    "result": {
        "id": 4,
        "message": "Call client to discuss site visit feedback.",
        "remind_at": "2026-05-22T00:00:00.000000Z",
        "assigned_to": 2
    }
}
```

**Error Responses**

| HTTP | `message` |
|------|-----------|
| `403` | `You are not authorized to add reminders to this lead.` |

---

### 36. Mark Reminder Done

| | |
|---|---|
| **Method** | `PUT` |
| **Endpoint** | `/api/reminders/{id}/done` |
| **Auth** | Bearer token |
| **Roles** | Admin, Assigned Staff (own reminders only) |

No request body required.

**Success Response** `200`
```json
{
    "status": true,
    "message": "Reminder marked as done."
}
```

**Error Responses**

| HTTP | `message` |
|------|-----------|
| `403` | `You are not authorized to update this reminder.` |
| `422` | `This reminder is already marked as done.` |

---

## Quotes

### 37. List Quotes

| | |
|---|---|
| **Method** | `GET` |
| **Endpoint** | `/api/leads/{id}/quotes` |
| **Auth** | Bearer token |
| **Roles** | Admin, Assigned Staff |

**Success Response** `200`
```json
{
    "status": true,
    "message": "Quotes fetched.",
    "result": {
        "quotes": [
            {
                "id": 1,
                "lead_id": 1,
                "property_id": 7,
                "property_title": "3BHK Flat in Vijay Nagar",
                "quoted_price": 4500000.0,
                "quote_date": "2026-05-18",
                "shared_by": "Rahul Sharma",
                "notes": "Price negotiable, owner flexible.",
                "created_by": "Rahul Sharma",
                "created_at": "2026-05-18T11:00:00.000000Z"
            }
        ],
        "count": 1
    }
}
```

**Error Responses**

| HTTP | `message` |
|------|-----------|
| `403` | `Forbidden.` |

---

### 38. Log Quote

| | |
|---|---|
| **Method** | `POST` |
| **Endpoint** | `/api/leads/{id}/quotes` |
| **Auth** | Bearer token |
| **Roles** | Admin, Assigned Staff |

**Request Body**

| Field | Type | Required | Rules |
|-------|------|----------|-------|
| `property_id` | integer | Yes | Must exist in `properties` |
| `quoted_price` | numeric | Yes | Min 1 |
| `quote_date` | date | Yes | Valid date (`YYYY-MM-DD`) |
| `shared_by` | string | Yes | Max 255 chars |
| `notes` | string | No | Max 2000 chars |

**Success Response** `201`
```json
{
    "status": true,
    "message": "Quote logged successfully.",
    "result": {
        "id": 2,
        "lead_id": 1,
        "property_id": 7,
        "property_title": "3BHK Flat in Vijay Nagar",
        "quoted_price": 4200000.0,
        "quote_date": "2026-05-20",
        "shared_by": "Rahul Sharma",
        "notes": null,
        "created_by": "Rahul Sharma",
        "created_at": "2026-05-20T10:00:00.000000Z"
    }
}
```

**Error Responses**

| HTTP | `message` |
|------|-----------|
| `403` | `Forbidden.` |

---

## Agreements

### 39. Record Final Deal

| | |
|---|---|
| **Method** | `POST` |
| **Endpoint** | `/api/leads/{id}/agreement` |
| **Auth** | Bearer token |
| **Roles** | Admin, Assigned Staff |

Records a final sale or rental deal for a lead. Each lead can have only one agreement. For `deal_type: rental`, the rental-specific fields become required.

> **Note:** PDF document upload is not supported via the API. Use the web panel to attach agreement documents.

**Request Body**

| Field | Type | Required | Rules |
|-------|------|----------|-------|
| `property_id` | integer | Yes | Must exist in `properties` |
| `deal_type` | string | Yes | `sale` or `rental` |
| `client_name` | string | Yes | Max 255 chars |
| `owner_name` | string | Yes | Max 255 chars |
| `agreed_price` | numeric | Yes | Min 1 |
| `deal_date` | date | Yes | Valid date (`YYYY-MM-DD`) |
| `notes` | string | No | Max 2000 chars |
| `agreement_start_date` | date | Rental only | Valid date |
| `agreement_end_date` | date | Rental only | After `agreement_start_date` |
| `monthly_rent` | numeric | Rental only | Min 1 |
| `rent_escalation_clause` | string | No | Max 500 chars |
| `expiry_reminder_days` | integer | Rental only | 1 – 365 |

**Success Response (Sale)** `201`
```json
{
    "status": true,
    "message": "Final deal recorded successfully.",
    "result": {
        "id": 1,
        "lead_id": 1,
        "property_id": 7,
        "property_title": "3BHK Flat in Vijay Nagar",
        "deal_type": "sale",
        "client_name": "Ankit Patel",
        "owner_name": "Ramesh Sharma",
        "agreed_price": 4300000.0,
        "deal_date": "2026-05-20",
        "notes": null,
        "created_by": "Rahul Sharma",
        "created_at": "2026-05-20T10:00:00.000000Z"
    }
}
```

**Success Response (Rental)** `201`

Same shape as sale, with additional rental fields appended:
```json
{
    "status": true,
    "message": "Final deal recorded successfully.",
    "result": {
        "...": "...(sale fields above)...",
        "agreement_start_date": "2026-06-01",
        "agreement_end_date": "2027-05-31",
        "monthly_rent": 18000.0,
        "rent_escalation_clause": "5% annual increase",
        "expiry_reminder_days": 30
    }
}
```

**Error Responses**

| HTTP | `message` |
|------|-----------|
| `403` | `Forbidden.` |
| `422` | `A final deal has already been recorded for this lead.` |
| `422` | `Validation failed.` (+ `errors`) |

---

## Documents

### 40. List Entity Documents

| | |
|---|---|
| **Method** | `GET` |
| **Endpoint** | `/api/entities/{type}/{id}/documents` |
| **Auth** | Bearer token |
| **Roles** | Admin (all), Staff (limited by entity type — see below) |

Returns all documents attached to any supported entity.

**Path Parameters**

| Param | Values | Staff Access |
|-------|--------|--------------|
| `type` | `property`, `owner`, `client`, `agreement` | `property` (published only), `owner` (all), `client` (assigned only), `agreement` (admin only) |
| `id` | Entity ID | — |

**Success Response** `200`
```json
{
    "status": true,
    "message": "Documents fetched successfully.",
    "result": [
        {
            "id": 4,
            "document_type": "Sale Deed",
            "original_filename": "sale-deed-ramesh.pdf",
            "mime_type": "application/pdf",
            "file_size_kb": 210,
            "file_size": "210 KB",
            "uploaded_by": "Admin",
            "uploaded_at": "2026-05-19T10:00:00.000000Z",
            "download_url": "https://domain.com/api/documents/4/download"
        }
    ]
}
```

**Error Responses**

| HTTP | `message` |
|------|-----------|
| `403` | `You are not authorized to access documents for this entity.` |
| `422` | `Invalid entity type. Allowed: property, owner, client, agreement` |

---

### 41. Download Document

| | |
|---|---|
| **Method** | `GET` |
| **Endpoint** | `/api/documents/{id}/download` |
| **Auth** | Bearer token |
| **Roles** | Admin, Staff (same access rules as entity) |

Streams the file as a binary download with the original filename. The `download_url` in the List Documents response contains the correct URL for each file.

**Success Response** `200`  
Binary file stream. `Content-Disposition: attachment; filename="sale-deed-ramesh.pdf"`

**Error Responses**

| HTTP | `message` |
|------|-----------|
| `403` | Forbidden (HTML abort) |
| `404` | `The requested resource was not found.` |

---

## Role-Based Access Summary

| Endpoint Group | Admin | Staff |
|----------------|-------|-------|
| Auth (login / logout / me) | ✓ | ✓ |
| Dashboard | ✓ global counts | ✓ own counts |
| Settings (property types, sourcing channels) | ✓ | ✓ |
| Owners (list, get) | ✓ | ✓ |
| Properties list / get | ✓ all | ✓ published only |
| Property image upload | ✓ | ✗ |
| Property images / documents (read) | ✓ | ✓ published only |
| Clients list | ✓ all | ✓ assigned only |
| Clients with requirements | ✓ all clients + all requirements | ✓ assigned clients + their requirements |
| Client get / update | ✓ | ✓ assigned only |
| Client create | ✓ | ✓ (auto-assigned to self) |
| Requirements (all) | ✓ | ✓ assigned clients only |
| Pipeline stages | ✓ | ✓ |
| Leads list | ✓ all | ✓ assigned only |
| Lead get / stage update | ✓ | ✓ assigned only |
| Shortlisted properties | ✓ | ✓ assigned leads only |
| Site visits (list / log) | ✓ | ✓ assigned leads only |
| Activity log | ✓ | ✓ assigned leads only |
| Notes (add) | ✓ | ✓ assigned leads only |
| Reminders list | ✓ all pending | ✓ own pending only |
| Reminders create / mark done | ✓ | ✓ assigned leads only |
| Quotes (list / log) | ✓ | ✓ assigned leads only |
| Agreements (record) | ✓ | ✓ assigned leads only |
| Entity documents (property, owner) | ✓ | ✓ (published/all) |
| Entity documents (client) | ✓ | ✓ assigned clients only |
| Entity documents (agreement) | ✓ | ✗ |

---

## Admin-Only Web Modules (No API Endpoints)

The following modules are accessible only via the Admin web panel. They are not exposed through the mobile API because they require Admin-level access and are not part of the mobile staff workflow.

| Module | Description |
|--------|-------------|
| **Rent Collection Tracking** | Month-by-month collection status for managed rental properties. Managed via web at `/admin/managed-rentals`. Properties managed by Agrawal Properties have `is_managed: true` in the property API response. |
| **Reports** | Property listings report, leads report, staff performance report, sourcing channel analytics, site visit history — available at `/admin/reports/*`. |
| **Settings** | Company settings, property types, lead stages, sourcing channels, document types, property tags, field templates, email templates — available at `/admin/settings/*`. |
| **Staff Management** | User accounts, activation/deactivation — available at `/admin/staff/*`. |
