# Client-Wise Comprehensive Report (single Lead, all related data, any status)

## Context

The user wants an "all-encompassing" report, scoped per client — confirmed to mean an individual
**Lead** (raw lead, verified lead, or converted client — **all statuses**, not just `status='client'`).
Today there is no single endpoint that rolls up everything tied to one Lead into one payload — data
is scattered across `UserAnalyticsController` (per-sales-rep KPIs), `LeadController::fetch_all_clients`
(list view, `status='client'` only, no nested detail), and various per-feature controllers (sales,
tasks, quotations, meetings...). This adds one new endpoint that, given any Lead id, returns every
related record in one JSON response, so it can back a "client/lead 360" report screen regardless of
where that lead currently sits in the Raw -> Verified -> Client pipeline.

Delivery: **POST** endpoint, JSON response (per user's answer — no PDF/Excel in this pass).
Content: Leads & follow-ups, Sales & payments, Tasks, Campaigns & activity logs — plus the other
directly-related records already modeled on Lead (notes, callbacks, quotations, meetings, demos,
visits, ownership history) since the user asked for "all the data."

## Endpoint

`POST /api/lead/report` — body: `{ "lead_id": <int> }`

- Permission: use the existing `report_management.client_analytics.*` module (the same one guarding
  `LeadController::lead_analytics` at `LeadController.php:3036`) rather than the per-status
  `lead_management.*` modules — this endpoint is a report, so it belongs under `report_management`.
  Check `$user->can('report_management.client_analytics.read')` /
  `$user->can('report_management.client_analytics.override')`; works uniformly regardless of the
  target lead's status (raw/verified/client).
- Scope check: if the requester only has `.read` (not `.override`), verify the lead's
  `created_by`/`assigned_to` is within `accessibleUserIdsFor($user)` (same helper used at
  `LeadController.php:2196`, from `app/Traits/ResolvesAccessibleUserIds.php`) — else 403.
- Validate `lead_id` exists, `is_deleted = 0` — else 404 via existing `ApiResponse` trait's `error()`.
  No `status` restriction — works for raw, verified, and client leads alike.

## Implementation

Add a new method `client_report(Request $request)` in `LeadController` (co-located with
`fetch_all_clients`/`lead_analytics`, reusing the same permission/scoping conventions), OR a small
`ClientReportService` if the aggregation logic gets long — lean toward a service class per
CLAUDE.md's "business logic belongs in Service classes" rule:

- New: `app/Services/ClientReportService.php` with `build(Lead $lead): array`.
- Controller method just resolves the lead, checks permission/scope, delegates to the service, wraps
  in `$this->success(...)`.

### Data assembled per client (all filtered `is_deleted = 0` where that column exists)

| Section | Source | Relation / query |
|---|---|---|
| Client profile | `Lead` | base fields + `company`, `phones`, `emails`, `creator`, `assignee` |
| Follow-ups | `LeadFollowup` (table `lead_follow_ups`) | `$lead->hasMany` via new relation (not yet on `Lead` model — add it) |
| Notes | `LeadNote` | add `notes()` relation to `Lead` |
| Callbacks | `LeadCallback` | existing `lead_call_back()` relation |
| Tasks | `Task` | add `tasks()` relation to `Lead` (belongsTo exists on `Task`, missing inverse on `Lead`) |
| Quotations | `Quotation` | existing `quotation()` relation |
| Sales + payments | `LeadSale` -> `sale_payment()` | existing `leadSales()` relation, eager-load `sale_payment`, `product`, `approval_statuses` |
| Meetings | `Meeting` | add `meetings()` relation to `Lead` |
| Demos | `Demo` | add `demos()` relation to `Lead` |
| Visits | `Visit` | add `visits()` relation to `Lead` |
| Projects | `Project` | add `projects()` relation to `Lead` |
| Ownership history | `LeadOwnershipHistory` | existing `leadOwnershipHistories()` relation |
| Credit exceeded requests | `LeadCreditExceededRequest` | existing `leadCreditExceededRequests()` relation |
| Campaign engagement | `CampaignRecipient` (+ `logs()` -> `CampaignLog`) | new query `CampaignRecipient::where('lead_id', $lead->id)->with('campaign', 'logs')` (no inverse relation exists on `Lead` yet — add `campaignRecipients()`) |
| Activity log | Spatie `Activity` model (`spatie/laravel-activitylog`, already a dependency) | `Activity::where('subject_type', Lead::class)->where('subject_id', $lead->id)->latest()->get()` — confirm during implementation whether `Lead` writes activity log entries anywhere (none found in `Lead.php`); if none exist, section returns empty array rather than being dropped, so the response shape stays stable |

Most of these `hasMany` relations don't exist on `Lead` yet (only `lead_services`, `lead_call_back`,
`phones`, `emails`, `quotation`, `leadSales`, `leadCreditExceededRequests`,
`leadOwnershipHistories` are defined at `app/Models/Lead.php:64-112`). Add the missing ones
(`followUps`, `notes`, `tasks`, `meetings`, `demos`, `visits`, `projects`, `campaignRecipients`)
following the exact same style as the existing methods, using `belongsTo(Lead::class)` already present
on each child model (confirmed: `Task.php:48`, `Meeting.php:52`, `Demo.php:33`, `Visit.php:26`,
`Project.php:41`, `LeadNote.php:20`, `LeadFollowup.php:29`, `CampaignRecipient.php:41`).

### Response shape

Follow the project's standard envelope (`App\Traits\ApiResponse`):

```json
{
  "success": true,
  "message": "Client report generated successfully.",
  "data": {
    "client": { ...lead fields, company, phones, emails, creator, assignee },
    "follow_ups": [...],
    "notes": [...],
    "callbacks": [...],
    "tasks": [...],
    "quotations": [...],
    "sales": [ { ...sale fields, "payments": [...], "product": {...} } ],
    "meetings": [...],
    "demos": [...],
    "visits": [...],
    "projects": [...],
    "ownership_history": [...],
    "credit_exceeded_requests": [...],
    "campaigns": [ { "campaign": {...}, "recipient": {...}, "logs": [...] } ],
    "activity_log": [...],
    "summary": {
      "total_sales_value": 0,
      "total_paid": 0,
      "outstanding_balance": 0,
      "total_tasks": 0,
      "total_follow_ups": 0
    }
  }
}
```

`summary` block: derive from the already-loaded collections in PHP (no extra queries) — mirrors the
math already used in `Company::getOutstandingBalanceAttribute()` (`app/Models/Company.php:52-79`) but
scoped to this one lead's sales instead of all of a company's leads.

## Route

Add to `routes/app.php`, inside the existing `lead` group near line 208 (`/client/all/fetch`):

```php
Route::post('/report', [LeadController::class, 'client_report']);
```

## Docs to update (per CLAUDE.md discipline)

- `docs/api.md` — add the new `POST /api/lead/report` route, request/response shape.
- `docs/business-rules.md` — note the new report under "Analytics Reports Available" (~line 226-237).
- `docs/architecture.md` — mention new `ClientReportService` if the service-class approach is used.
- `docs/database.md` — only if any new relation requires a migration (none expected; all tables/FKs
  already exist — this is a pure read/aggregation feature).

## Verification

- Manually call `POST /api/lead/report` with a valid `lead_id` via Postman/curl using a real Sanctum
  token, for a lead at each status (raw, verified, client) — confirm all sections populate and payment
  math matches what's shown in the existing sales/finance screens for that lead.
- Test permission boundaries: user with only `.read` and lead outside their `accessibleUserIdsFor`
  scope should get 403; user with `.override` should see any lead regardless of status.
- Test a lead with zero sales/tasks/etc. — confirm empty arrays, not errors, and `summary` block
  reports zeros correctly.
- Run existing test suite (`php artisan test` or equivalent) to check nothing in `LeadController` or
  `Lead` model regressed from the new relation methods.
