# Forms

## Public Forms Found

Search targets: `resources/views`, `public/js`, `resources/js`, `resources/css`. No `resources/js` or `resources/css` directories exist.

Public/non-dashboard forms found:

- Lead/contact form: `resources/views/pages/contact.blade.php`, `<form id="contact-form">`
- Quote calculator form: `resources/views/pages/calculator.blade.php`, `<form id="quote-calculator-form">`
- Region banner language form: `resources/views/components/region-banner.blade.php`, `<form id="region-banner-form">`
- Service pricing calculator form: `resources/views/components/service-pricing-calculator.blade.php`, `<form id="pricingCalculator">`

Auth/dashboard forms are documented in `docs/auth-dashboard.md`.

## Main Lead Form

### Location

- Blade: `resources/views/pages/contact.blade.php`
- Form id: `contact-form`
- HTML action: localized `contact.submit`
- HTML method: `POST`
- Main JS: `public/js/lead-tracking.js`
- Layout loading JS: `resources/views/layouts/app.blade.php`
- Controller fallback/native POST: `App\Http\Controllers\ContactController::submit`
- JS submit endpoint: `POST /api/leads/submit`, route `leads.submit`, controller `App\Http\Controllers\LeadController::submitForm`

Important behavior: the frontend intercepts submit in `public/js/lead-tracking.js`, prevents default, validates client-side, and sends JSON to `/api/leads/submit`. If JavaScript is disabled or fails before binding, the form posts to `ContactController::submit`. Both paths save submitted leads into the `leads` table.

### Fields

Visible user fields in `resources/views/pages/contact.blade.php`:

- `services[]`: checkbox array. Options: `web-development`, `ecommerce`, `shopify`, `seo-sem`, `social-media`, `branding`, `advertising`, `digital-marketing`, `hosting`, `optimization`.
- `timeframe`: radio. Options: `morning`, `afternoon`, `evening`, `flexible`.
- `name`: text, required.
- `email`: email, required.
- `phone_country`: select, optional.
- `phone`: tel, shown optional in UI.
- `website`: text, optional.
- `message`: textarea, shown optional in UI.
- `fax_number`: hidden honeypot spam trap; legitimate users leave it empty.

Hidden tracking/calculator fields:

- `service_slug`, `service_name`, `industry_slug`, `industry_name`
- `selected_project_type`, `selected_scope`, `selected_addons`, `selected_budget_range`
- `detected_country`, `market_multiplier`, `complexity_level`, `estimated_hours`, `estimated_price_min`, `estimated_price_max`
- `entry_page`, `first_page`, `referrer`
- `utm_source`, `utm_medium`, `utm_campaign`, `utm_term`, `utm_content`
- `gclid`, `fbclid`, `msclkid`
- `entry_service_hint`, `entry_intent_hint`, `recommended_service`, `session_language`, `generated_summary`

`public/js/lead-tracking.js` adds `lead_uuid` at submit time from localStorage key `criazo_lead_uuid`.

### Client Validation

File: `public/js/lead-tracking.js`, class `LeadTracker`, method `validateForm`.

Rules:

- All `input[required]` and `textarea[required]` must be non-empty. In current Blade, `name`, `email`, and `timeframe` inputs are required; `message` is not required in HTML.
- Email must match `/^[^\s@]+@[^\s@]+\.[^\s@]+$/`.
- Phone is validated only if filled:
  - market `us`: exactly 10 digits.
  - market `brazil`: 10 to 11 digits.
  - market `europe`/`other`: regex `/^[+]?[\d\s\-()]{8,20}$/` and at least 8 digits.
- At least one `services[]` checkbox must be selected.
- One `timeframe` radio must be selected.

Phone and message are optional consistently across UI labels, frontend validation, AJAX validation, and fallback `/contact` validation.

### Server Validation: Lead API Submit

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

Method: `submitForm` calls private `submitFormHandler`.

Rules in `submitFormHandler`:

- `lead_uuid`: nullable string
- `fax_number`: prohibited honeypot
- `name`: required string max 255
- `email`: required email max 255
- `phone`: nullable string max 50 regex `/^[+]?[\d\s\-().]{8,50}$/`
- `company`: nullable string max 255
- `website`: nullable string max 255
- `service_slug`, `service_name`, `industry_slug`, `industry_name`: nullable string
- `services`: required array min 1
- `services.*`: string
- `message`: nullable string
- `budget`: nullable string
- `timeline`: nullable string
- `timeframe`: required string
- Calculator/tracking fields listed above: nullable strings with max constraints on selected fields.

Saved model/table:

- Model: `App\Models\Lead`
- Table: `leads`
- Events model: `App\Models\LeadEvent`
- Events table: `lead_events`

Success handling:

- Updates/creates `Lead`.
- Sets `status=submitted`, `form_completion_percentage=100`, `submitted_at=now()`.
- Logs event `LeadEvent::TYPE_FORM_SUBMITTED`.
- Broadcasts `App\Events\LeadNotification` with event `lead.submitted`.
- Calls `App\Services\LeadBeamsNotificationService::notifySubmitted`.
- Returns JSON `{ success: true, message, lead_id, redirect_url }`.
- JS redirects to `redirect_url` or fallback `/thank-you`.

Error handling:

- Validation exceptions return normal Laravel JSON 422 when requested with `Accept: application/json`.
- Database/save exceptions are logged and return JSON 500 with `success:false`.
- Notification exceptions are logged separately and do not block successful submission.
- If `config('leads.paused')` is true, returns JSON `{success:false, paused:true}` with status `503`.

Spam/rate-limit protection:

- Hidden `fax_number` honeypot in `resources/views/pages/contact.blade.php`.
- `LeadController::submitFormHandler` rejects filled honeypot submissions.
- Named rate limiter `lead-api` is configured in `app/Providers/RouteServiceProvider.php`.
- `routes/web.php` applies `throttle:lead-api` to `api/leads/*`.
- `api/leads/*` is CSRF-exempt in `app/Http/Middleware/VerifyCsrfToken.php`.

Notifications/webhooks:

- Broadcast + Pusher Beams only.
- No email or webhook sender found.

## Contact Fallback POST

### Location

- Route: `POST /contact`, name `contact.submit`
- Localized route: `POST /{locale}/contact`, name `contact.submit.localized`
- Controller: `App\Http\Controllers\ContactController::submit`
- Form action in Blade: `{{ $localizedRoute('contact.submit') }}`

### Validation

`ContactController::submit` rules:

- `name`: required string max 255
- `email`: required email
- `phone`: nullable string max 50 regex `/^[+]?[\d\s\-().]{8,50}$/`
- `phone_country`: nullable string size 2
- `services`: required array
- `timeframe`: required string
- `website`: nullable url max 255. Controller prepends `https://` if missing.
- `message`: nullable string
- `fax_number`: prohibited honeypot

### Storage

- Creates a submitted `Lead` row in the `leads` table.
- Logs a `LeadEvent::TYPE_FORM_SUBMITTED` event with source `contact_form_fallback`.
- Triggers the submitted lead broadcast and Pusher Beams notification path.
- Keeps a JSON backup in `storage/app/contact-submissions.json`.
- Keeps only last 500 JSON backup submissions.
- No email or webhook found.

### Success/Error

- If `config('leads.paused')` is true, redirects back with old input and flash `error`.
- On success, redirects to localized `contact.thank-you` if possible, fallback `/thank-you`, with flash `success`.
- Database save failures are logged and redirect back with flash `error`.
- JSON backup failures are logged and do not block redirect.

## Lead Tracking Partial Capture

File: `public/js/lead-tracking.js`

Routes/controllers:

- CTA click: `POST /api/leads/track-click` -> `LeadController::trackClick`
- Form started: `POST /api/leads/form-started` -> `LeadController::formStarted`
- Partial capture: `POST /api/leads/capture` -> `LeadController::captureFormData`
- Prefill: `GET /api/leads/data` -> `LeadController::getLeadData`
- Abandoned: `POST /api/leads/form-abandoned` -> `LeadController::formAbandoned`

Storage:

- Browser localStorage:
  - `criazo_lead_uuid`
  - `criazo_lead_data`
  - `criazo_cta_tracking`

Rules:

- A lead is not created on form start alone.
- Partial capture creates/updates a lead only after meaningful data exists: name, email, or phone.
- Completion uses `Lead::calculateCompletion()` fields: `name`, `email`, `phone`, `company`, `service_slug`, `message`.
- Status becomes `warm` at completion >= 40 while cold, and `hot` at completion >= 80 for cold/warm/hot.

## Quote Calculator Form

### Location

- Blade: `resources/views/pages/calculator.blade.php`
- Form id: `quote-calculator-form`
- JS: `public/js/quote-calculator.js`
- Controller: `App\Http\Controllers\CalculatorController::index`
- Config repository: `App\Support\QuoteCalculator\ConfigRepository`
- Data files: `data/quote-calculator/*.php`

### Behavior

The calculator is client-side and does not submit to Laravel directly. It builds a contact URL with query parameters, then sends the user to the contact form.

Handoff fields generated in `public/js/quote-calculator.js` method `buildLeadPayload`:

- `source=calculator`
- `selected_service`
- `selected_project_type`
- `selected_scope`
- `selected_addons`
- `selected_budget_range`
- `selected_timeline`
- `detected_country`
- `market_multiplier`
- `complexity_level`
- `estimated_hours`
- `estimated_price_min`
- `estimated_price_max`
- `entry_page`
- `first_page`
- `referrer`
- `utm_source`, `utm_medium`, `utm_campaign`, `utm_term`, `utm_content`
- `gclid`, `fbclid`, `msclkid`
- `entry_service_hint`, `entry_intent_hint`, `recommended_service`, `session_language`, `generated_summary`
- `message`
- `service`

These become hidden/input values on `resources/views/pages/contact.blade.php` and are sent to `/api/leads/submit`.

## Region Banner Form

- Blade: `resources/views/components/region-banner.blade.php`
- Form id: `region-banner-form`
- Method/action: JavaScript intercepts submit; no HTML action.
- Fields: `locale`, `current_url`, CSRF token.
- JS builds `/language/{locale}?current_url={currentUrl}` and navigates.
- Route: `GET /language/{locale}`, name `language.switch`, controller `App\Http\Controllers\LanguageController::switch`.
- It is not a lead form and does not store lead data.

## Service Pricing Calculator Form

- Blade: `resources/views/components/service-pricing-calculator.blade.php`
- Form id: `pricingCalculator`
- Method/action: no submit endpoint.
- Fields: `scope` radio, `addon` checkboxes.
- JS recalculates visible total price only.
- CTA links to localized contact URL with `?service={slug}`.
- It is not a database submission form.

## Flow

```mermaid
flowchart TD
    A["CTA with data-track-cta"] --> B["LeadTracker::handleCTAClick"]
    B --> C["POST /api/leads/track-click"]
    C --> D["Lead created/updated + click event"]
    B --> E["Navigate to /contact with query params"]
    E --> F["Contact form hidden fields prefilled"]
    F --> G["Blur/input -> /api/leads/capture"]
    G --> H["Partial lead + field_filled event"]
    F --> I["Submit -> /api/leads/submit"]
    I --> J["Lead submitted + notification"]
    J --> K["Thank-you redirect"]
```
