# Leads And Dashboard

## Lead Model

File: `app/Models/Lead.php`

Class: `App\Models\Lead`

Table: `leads`

Fillable:

- Contact: `uuid`, `name`, `email`, `phone`, `company`
- Interest: `service_slug`, `service_name`, `industry_slug`, `industry_name`, `pricing_tier`, `pricing_value`, `message`, `budget`, `timeline`
- Tracking: `source_page`, `source_button`, `utm_source`, `utm_medium`, `utm_campaign`, `referrer`, `referrer_domain`, `ip_address`, `user_agent`, `device_type`, `locale`, `country`, `region`, `city`, `browser`, `os`, `timezone`
- Calculator/enrichment: `selected_project_type`, `selected_scope`, `selected_addons`, `selected_budget_range`, `detected_country`, `market_multiplier`, `complexity_level`, `estimated_hours`, `estimated_price_min`, `estimated_price_max`, `first_page`, `utm_term`, `utm_content`, `gclid`, `fbclid`, `msclkid`, `entry_service_hint`, `entry_intent_hint`, `recommended_service`, `session_language`, `generated_summary`
- Status/timestamps: `status`, `form_completion_percentage`, `first_interaction_at`, `last_interaction_at`, `submitted_at`

Casts:

- `first_interaction_at`: datetime
- `last_interaction_at`: datetime
- `submitted_at`: datetime

Relationship:

- `events()`: `hasMany(App\Models\LeadEvent::class)`

Scopes:

- `hot()`: status `hot`
- `cold()`: status in `cold`, `warm`
- `submitted()`: status `submitted`
- `abandoned()`: status in `cold`, `warm`, `hot`, completion > 0, no `submitted_at`, inactive at least 3 days
- `market($market)`: market segmentation using `config/leads.php`
- `today()`, `thisWeek()`, `thisMonth()`

Helpers:

- `isSubmitted()`
- `isAbandoned()`
- `getStatusColorAttribute()`
- `getStatusLabelAttribute()`
- `calculateCompletion()`
- `updateCompletion()`
- `logEvent(string $type, ?string $source, ?array $data)`

Status labels/colors in model:

- `cold`: gray
- `warm`: yellow
- `hot`: orange
- `submitted`: blue
- `contacted`: purple
- `converted`: green
- `lost`: red
- `archived`: slate

Schema fix: `database/migrations/2026_05_09_000001_harden_leads_dashboard_and_schema.php` ensures MySQL status enums include `archived`. SQLite stores the enum as a string.

## Lead Event Model

File: `app/Models/LeadEvent.php`

Class: `App\Models\LeadEvent`

Table: `lead_events`

Fillable:

- `lead_id`
- `event_type`
- `event_source`
- `event_data`
- `page_url`
- `page_title`
- `created_at`

Casts:

- `event_data`: array
- `created_at`: datetime

Relationship:

- `lead()`: `belongsTo(App\Models\Lead::class)`

Event constants:

- `TYPE_CLICK = click`
- `TYPE_PAGE_VIEW = page_view`
- `TYPE_FORM_START = form_start`
- `TYPE_FIELD_FILLED = field_filled`
- `TYPE_FORM_ABANDONED = form_abandoned`
- `TYPE_FORM_SUBMITTED = form_submitted`

## Lead Controller

File: `app/Http/Controllers/LeadController.php`

Class: `App\Http\Controllers\LeadController`

Methods:

- `trackClick(Request $request): JsonResponse`
- `captureFormData(Request $request): JsonResponse`
- `formStarted(Request $request): JsonResponse`
- `formAbandoned(Request $request): JsonResponse`
- `submitForm(Request $request): JsonResponse`
- `getLeadData(Request $request): JsonResponse`
- private `submitFormHandler(Request $request): JsonResponse`
- private `findOrCreateLead(Request $request, ?string $uuid): Lead`
- private `applyEnrichment(Lead $lead, Request $request): void`
- private `ifPausedReturn(): ?JsonResponse`

Every public method exits early when `config('leads.paused')` is true.

## Dashboard Controller

File: `app/Http/Controllers/LeadDashboardController.php`

Class: `App\Http\Controllers\LeadDashboardController`

Actions:

- `index(Request $request)`: list/dashboard page.
- `show(Lead $lead)`: detail page.
- `updateStatus(Request $request, Lead $lead)`: JSON status update.
- `destroy(Lead $lead)`: delete one lead.
- `destroyBulk(Request $request)`: bulk delete by ids. Route exists; no visible bulk-select form was found in current `dashboard/leads/index.blade.php`.
- `export(Request $request)`: streamed CSV.

Private stat methods:

- `getStatistics(string $dateRange, ?string $market = null)`
- `getMarketBreakdown(string $dateRange)`
- `getTopSources(string $dateRange, ?string $market = null)`
- `getTopReferralSources(string $dateRange, ?string $market = null)`
- `getServiceBreakdown(string $dateRange, ?string $market = null)`

## Dashboard Routes

File: `routes/web.php`

All under `auth` and `admin` middleware and `/dashboard` prefix:

- `GET /dashboard/leads`, `dashboard.leads.index`, `LeadDashboardController::index`
- `GET /dashboard/leads/export`, `dashboard.leads.export`, `LeadDashboardController::export`
- `POST /dashboard/leads/bulk-delete`, `dashboard.leads.bulk-delete`, `LeadDashboardController::destroyBulk`
- `GET /dashboard/leads/{lead}`, `dashboard.leads.show`, `LeadDashboardController::show`
- `PATCH /dashboard/leads/{lead}/status`, `dashboard.leads.update-status`, `LeadDashboardController::updateStatus`
- `DELETE /dashboard/leads/{lead}`, `dashboard.leads.destroy`, `LeadDashboardController::destroy`

Permissions:

- Dashboard routes require an authenticated user with `users.is_admin = true`.
- Middleware: `app/Http/Middleware/EnsureAdmin.php`
- Alias: `admin` in `app/Http/Kernel.php`

## Leads Listing View

File: `resources/views/dashboard/leads/index.blade.php`

Data passed from `LeadDashboardController::index`:

- `$leads`
- `$stats`
- `$topSources`
- `$topReferralSources`
- `$serviceBreakdown`
- `$marketBreakdown`
- `$status`
- `$market`
- `$dateRange`
- `$search`

Filters:

- `search`: matches `name`, `email`, `company`, `phone`
- `status`: `submitted`, `hot`, `warm`, `cold`, `abandoned`, `contacted`, `converted`, `lost`, `archived`
- `market`: `us`, `europe`, `brazil`, `other`
- `range`: `7`, `30`, `90`, `all`

Pagination:

- Controller uses `paginate(20)`.
- View renders `$leads->withQueryString()->links()`.

Sort:

- Controller orders `created_at desc`.

Cards show:

- Name/initials, status, email, market, service, source, device, completion percentage, relative created time.
- Detail link: `dashboard.leads.show`.
- Email quick link: `mailto:{email}` when present.
- Dropdown actions: change status, archive/unarchive, delete.

Stats cards:

- All leads: `$stats['total']`
- Submitted: `$stats['submitted']`
- Hot: `$stats['hot']`
- Warm: `$stats['warm']`
- Cold: `$stats['cold']`
- Abandoned: `$stats['abandoned']`
- Archived: `$stats['archived']`

Sidebar widgets:

- Today: `$stats['today_total']`, `$stats['today_submitted']`, `$stats['conversion_rate']`
- Top sources: `$topSources` grouped by `source_page`
- Referral sources: `$topReferralSources` using `utm_source`, `referrer_domain`, or `Direct`
- By market: `$marketBreakdown`
- By service: `$serviceBreakdown`

JavaScript in view:

- Opens status modal.
- Sends `PATCH /dashboard/leads/{lead}/status` with CSRF token.
- Creates delete form dynamically for `DELETE /dashboard/leads/{lead}`.
- Archive/unarchive quick status update.
- Registers service worker `/service-worker.js`.
- Enables Pusher Beams push notifications.
- Subscribes to Pusher channel `leads` and binds `lead.submitted`, `lead.abandoned`, `lead.cold`.

## Lead Detail View

File: `resources/views/dashboard/leads/show.blade.php`

Route: `dashboard.leads.show`

Controller: `LeadDashboardController::show`

Loads:

- `$lead->load('events')`

Shows:

- Header with back link, user name, status select, delete button.
- Hero: name, status, completion, email/call buttons.
- Contact card: email, phone, company.
- Interest card: service, industry, package/pricing, budget, timeline.
- Message card if `message` exists.
- Tracking card: source page, referrer, source button, referrer domain, IP, device, browser, OS, country, city.
- Timeline/activity: created, submitted, last activity, last 10 events sorted descending.

JavaScript:

- `updateStatus(status)` sends `PATCH` to `dashboard.leads.update-status`, then reloads.

## Export

Route: `GET /dashboard/leads/export`

Controller: `LeadDashboardController::export`

Filters:

- `status`
- `market`
- `range`

CSV columns:

- ID, Name, Email, Phone, Company
- Service, Industry, Status, Market, Completion %
- Source Page, Source Button, Referrer, Referrer Domain
- Device, Browser, OS, Country, Region, City
- Locale, Timezone, IP, Created, Submitted

Implementation:

- Streams CSV with `response()->stream`.
- Chunks query in batches of 500.

## Data Flow

```mermaid
flowchart TD
    A["public/js/lead-tracking.js"] --> B["LeadController API endpoints"]
    B --> C["leads table via App\\Models\\Lead"]
    B --> D["lead_events table via Lead::logEvent"]
    C --> E["LeadDashboardController::index"]
    D --> F["LeadDashboardController::show loads events"]
    E --> G["dashboard/leads/index.blade.php"]
    F --> H["dashboard/leads/show.blade.php"]
    B --> I["LeadNotification broadcast"]
    I --> G
```

## Current Protections

- Dashboard access is limited to authenticated admins via `EnsureAdmin`.
- Lead API routes are throttled with `throttle:lead-api`.
- Form submit paths use a hidden `fax_number` honeypot.
- Calculator/enriched lead columns are present after migrations `2026_04_20_000001_add_calculator_fields_to_leads_table.php` and `2026_05_09_000001_harden_leads_dashboard_and_schema.php`.

## Remaining Technical Debt

- `destroyBulk` route exists but current listing UI does not show a matching bulk selection form.
- Lead API remains public by design for tracking and form submission, so monitor rate-limit logs and consider captcha if spam persists.
