# Event Lead Capture Form Implementation Guide

**Last Updated:** 2026-02-08

## Overview

The Event Lead Capture Form is a tablet-optimized landing page designed for efficient lead collection at conferences and events. It features:

- Custom HTML form (not embedded HubSpot form) for full UX control
- Business card scanning via camera API + OCR
- Offline submission capability with automatic sync
- Tablet/mobile optimization (44px touch targets, large buttons)
- Speed optimizations for event use
- Owner name tracking via `utm_medium__c` for HubSpot workflow-based distribution

## Simplified Tablet/Mobile Versions

### Overview

Simplified versions (`-tablet` or `-mobile` suffix) provide a streamlined experience optimized for tablet/mobile use at events:

- **Minimal branding:** Small header with logo and event name only
- **No distractions:** No footer, no website navigation, no hero section
- **Same functionality:** All form features work identically (owner selection, OCR, offline sync)
- **Component reuse:** Uses same form components as full version for automatic updates

### When to Use

**Use Full Version (`/events/{event-id}`) when:**

- Sharing links with leads via email/messaging
- SEO/marketing purposes
- Need full website context and navigation

**Use Simplified Version (`/events/{event-id}-tablet`) when:**

- Using tablets on-site at events
- Need minimal UI without distractions
- Quick lead capture workflow
- Team members prefer streamlined interface

### Technical Details

- **Header:** Minimal sticky header (60px mobile, 80px tablet) with logo and event name
- **No dependencies:** Works without Tailwind CSS or Alpine.js (form CSS/JS are standalone)
- **Same components:** Form, owner selection, OCR, offline sync all use shared components
- **Performance:** Faster load time due to minimal HTML/CSS (no full website assets)

## Architecture

### File Structure

```
v2/
├── pages/
│   └── event-lead-capture.php          # Main page
├── api/
│   └── event-lead-capture.php          # Form submission endpoint
│   └── ocr-business-card.php           # OCR processing endpoint
├── components/
│   └── event-form.php                  # Reusable form component
├── js/
│   └── event-form.js                  # Form logic & validation
│   └── business-card-scanner.js       # Camera & OCR integration
│   └── offline-sync.js                # Offline storage & sync
└── css/
    └── event-form.css                 # Tablet-optimized styles
```

### Data Flow

1. User opens page → Form loads
2. Optional: User scans business card → OCR processes → Auto-fills form
3. User fills form manually or edits auto-filled data
4. Form validates → Submits to API endpoint
5. API submits to HubSpot Forms API v3 → Assigns owner → Returns success
6. Success state shown → "Add Another" option for quick re-entry

### Simplified Tablet/Mobile Versions

**Overview:**
Simplified versions (`-tablet` or `-mobile` suffix) provide a streamlined experience optimized for tablet/mobile use at events:

- **Minimal branding:** Small header with logo and event name only
- **No distractions:** No footer, no website navigation, no hero section
- **Same functionality:** All form features work identically (owner selection, OCR, offline sync)
- **Component reuse:** Uses same form components as full version for automatic updates

**When to Use:**

- **Full Version** (`/events/{event-id}`): Sharing links with leads, SEO/marketing, need full website context
- **Simplified Version** (`/events/{event-id}-tablet`): On-site tablet use, minimal UI, quick lead capture workflow

**Technical Details:**

- **Header:** Minimal sticky header (60px mobile, 80px tablet) with logo and event name
- **No dependencies:** Works without Tailwind CSS or Alpine.js (form CSS/JS are standalone)
- **Same components:** Form, owner selection, OCR, offline sync all use shared components
- **Performance:** Faster load time due to minimal HTML/CSS (no full website assets)

## Form Fields

### Required Fields

- **Customer Type** (`customer_type__c`): Dropdown - Neukunde / Bestandskunde
- **First Name** (`firstname`): Text input
- **Last Name** (`lastname`): Text input
- **Company** (`company`): Text input
- **Email** (`email`): Email input
- **Owner** (`owner`): Hidden input - Selected via owner selection screen before form display
  - **Intergastra 2026:** Barti, Daniela, David, Felix, Freddie, Kathrin, Lena, Michael, Quirin
  - **Internorga 2026:** Barti, Constantin, Daniela, David, Fabian, Felix, Freddie, Jakob, John, Lena, Michelle, Muhammed, Quirin
  - **Selection**: Grid-based selection screen appears first, form displays after selection

### Optional Fields

- **Salutation** (`salutation`): Dropdown - Herr / Frau / Divers
- **Phone** (`phone`): Tel input (auto-formatted)
- **Position** (`jobtitle`): Dropdown - Inhaber / Geschäftsführer, HR / Schichtleitung, Mitarbeiter, Sonstiges
- **Interested In** (`interested_in__c`): Multi-select feature grid (3-column) - All 13 options: Schichtplanung, Zeiterfassung, Payroll Plus, Abwesenheiten, Digitale Personalakte, Dokumentenmanagement, AI Agent, QR-Code, Trinkgeld, Personaldienstleister, Hardware Terminals, Events, Checklisten
  - **Layout:** 3-column grid on tablet/desktop, 2-column on mobile
  - **Visual Design:** Button-style cards with icons and labels
  - **Selection:** Multi-select with visual feedback (blue border and background when selected)
  - **Icons:** Each feature has a unique Heroicons-style SVG icon
  - **Ordering:** Features ordered from most to least popular
- **Additional Notes** (`description`): Textarea

### Hidden Tracking Fields

Set automatically by API:

- `sign_up_type__c`: "Event Lead Capture"
- `content`: Event name (if provided)
- `hs_lead_status`: "NEW"
- `record_type_id__dropdown_`: "012aa0000013oZCAAY" (Inbound)
- `source__c`: Always "Trade Fair" for all event submissions
- `leadsource`: Always "trade fair" for all event submissions (Internorga, Intergastra, etc.)
- `utm_medium__c`, `utm_campaign__c`, `utm_term__c`, `content__c`, `gclid__c`: UTM parameters

## Usage

### Basic Page Access

```
/v2/pages/event-lead-capture.php
```

### With Event Parameters

```
/v2/pages/event-lead-capture.php?event=Intergastra%202026&event_id=intergastra-2026&owner=Felix&date=7-11%20Februar%202026&location=Messe%20Stuttgart
```

### Pre-configured Event Routes

**Intergastra 2026:**

- Full version: `/events/intergastra-2026`
- Simplified tablet/mobile: `/events/intergastra-2026-tablet` or `/events/intergastra-2026-mobile`
- Short URLs: `/intergastra-2026` or `/intergastra` (redirects to primary)
- Dates: February 7-11, 2026
- Location: Messe Stuttgart, Stuttgart, Germany

**Internorga 2026:**

- Primary URL: `/events/internorga-2026`
- Short URLs: `/internorga-2026` or `/internorga` (redirects to primary)
- Dates: March 13-17, 2026
- Location: Hamburg Messe und Congress, Hamburg, Germany

**URL Parameters:**

- `event`: Event name (displayed in hero, used for tracking)
- `event_id`: Event identifier (stored in HubSpot)
- `owner`: Owner name or ID (assigned to contact after submission)
- `date`: Event date (displayed in hero)
- `location`: Event location (displayed in hero)

## API Endpoints

### Form Submission

**Endpoint:** `/v2/api/event-lead-capture.php`

**Method:** POST

**Content-Type:** application/json

**Request Body:**

```json
{
  "firstname": "Max",
  "lastname": "Mustermann",
  "email": "max@beispiel.de",
  "company": "Muster GmbH",
  "customer_type__c": "Neukunde",
  "salutation": "Herr",
  "phone": "+49 123 456789",
  "jobtitle": "Inhaber / Geschäftsführer",
  "interested_in__c": "Schichtplanung,Zeiterfassung", // Frontend sends comma-separated, backend converts to semicolon-separated
  "description": "Interessiert an Ordio",
  "event_name": "Intergastra 26",
  "event_id": "intergastra-2026",
  "owner": "Felix",
  "hubspotutk": "abc123...",
  "utm_source": "event",
  "utm_campaign": "intergastra-2026",
  "page_url": "/v2/pages/event-lead-capture.php",
  "referrer": ""
}
```

**Response (Success):**

```json
{
  "success": true,
  "message": "Contact saved successfully",
  "data": {
    "contactId": "12345678",
    "eventName": "Intergastra 26"
  }
}
```

**Response (Error):**

```json
{
  "success": false,
  "message": "Failed to save contact. Please try again.",
  "error": "Detailed error message"
}
```

### OCR Processing

**Endpoint:** `/v2/api/ocr-business-card.php`

**Method:** POST

**Content-Type:** multipart/form-data

**Request:**

- `image`: Image file (JPEG, PNG, WebP, max 5MB)

**Response:**

```json
{
  "success": true,
  "message": "OCR processing completed",
  "data": {
    "firstname": "Max",
    "lastname": "Mustermann",
    "email": "max@beispiel.de",
    "phone": "+49123456789",
    "company": "Muster GmbH",
    "jobtitle": "Geschäftsführer"
  }
}
```

**Note:** OCR endpoint uses Google Cloud Vision API for text detection. Configure `GOOGLE_VISION_API_KEY` environment variable or it will fallback to `GOOGLE_MAPS_API_KEY` if using the same GCP project.

## Duplicate Prevention

### Multi-Layer Defense Strategy

The system uses a **comprehensive multi-layer defense** to prevent duplicate submissions, addressing race conditions that previously caused duplicate contacts with identical timestamps.

**Architecture:**

1. **Layer 1: In-Memory Cache** (Fastest - prevents rapid duplicates)
2. **Layer 2: File-Based Locking** (Prevents concurrent requests)
3. **Layer 3: HubSpot API Check** (Most accurate but slower)
4. **Frontend Protection** (User experience)

### API Endpoint Duplicate Detection

The API endpoint (`event-lead-capture.php`) includes comprehensive duplicate prevention:

**Layer 1: In-Memory Cache**

- Static array tracks submissions in progress
- Key: `email|event_id|event_name`
- Auto-cleanup after 5 minutes
- Prevents rapid duplicates within same request cycle

**Layer 2: File-Based Locking**

- Uses `flock()` with non-blocking exclusive locks
- Lock files in `writable/locks/event-submission-{hash}.lock`
- Prevents concurrent requests for same email+event
- Auto-cleanup of stale locks (>60 seconds)
- Lock automatically released in finally block

**Layer 3: HubSpot API Check**

- Checks HubSpot CRM API for recent contacts
- **Time window:** 10 minutes (extended from 5 to account for API indexing delay)
- Uses `hs_createdate` property (more reliable than `createdate`)
- Retry logic for transient errors (2 retries with 1-second delay)
- Filters by:
  - Same email address (case-insensitive)
  - `sign_up_type__c` = "Event Lead Capture"
  - Created within last 10 minutes
  - Same event (by `content` field matching event name)

**How It Works:**

1. **In-Memory Check:** Fast check against static array (prevents rapid duplicates)
2. **File Lock Acquisition:** Non-blocking exclusive lock (prevents concurrent requests)
3. **HubSpot API Check:** Query HubSpot for recent duplicates (most accurate)
4. **Submission:** If all checks pass, submit to HubSpot Forms API v3
5. **Cleanup:** Finally block always releases lock and cleans up tracking

**If Duplicate Found:**

- Returns success response without creating new contact
- Logs duplicate detection event with layer information
- Returns existing contact ID
- Releases lock and cleans up tracking

**Benefits:**

- Prevents duplicate contacts in HubSpot (including race conditions)
- Handles rapid double-clicks on submit button
- Prevents concurrent requests from multiple tabs/windows
- Works alongside frontend duplicate prevention
- Comprehensive logging for debugging

### Frontend Duplicate Prevention

The frontend (`event-form.js`) includes multiple layers of duplicate prevention:

1. **Request ID Generation:** Unique ID (`{timestamp}-{random}`) for each submission
2. **SessionStorage Tracking:** Prevents cross-tab duplicates (30-second window)
3. **`isSubmitting` Flag:** Prevents form submission if already submitting
4. **Timestamp Tracking:** Minimum 2 seconds between submission attempts
5. **Form Disabling:** Form disabled immediately on submit (before validation)
6. **Visual Indicator:** Opacity reduction during submission
7. **Button Disabling:** Submit button disabled immediately on click

**SessionStorage Protection:**

- Key: `event_submission_{email}_{eventId}`
- Stores: `{timestamp, requestId}`
- Checked before submission
- Cleared on success or error
- Prevents duplicate submissions across browser tabs/windows

**Combined Protection:**

- Frontend prevents rapid double-clicks (2-second minimum interval)
- Frontend prevents cross-tab duplicates (sessionStorage)
- API Layer 1: In-memory cache prevents rapid duplicates
- API Layer 2: File locking prevents concurrent requests
- API Layer 3: HubSpot API prevents duplicates within 10-minute window
- Offline sync prevents duplicates in offline queue (hash-based)

### Monitoring & Debugging

**Scripts Available:**

1. **`monitor-event-form-submissions.php`**
   - Checks for duplicates in HubSpot
   - Detects race conditions (identical timestamps within 1 second)
   - Reports invalid Interested In values

2. **`investigate-duplicate-submissions.php`**
   - Investigates specific duplicate cases
   - Shows detailed comparison
   - Checks lock files and logs

3. **`test-concurrent-submissions.php`**
   - Tests file locking behavior
   - Tests in-memory tracking
   - Tests lock cleanup

**See:** `docs/systems/forms/DUPLICATE_PREVENTION_IMPLEMENTATION.md` for complete implementation details.

## Interested In Field Format

### HubSpot Property Configuration

The `interested_in__c` field is a **checkbox** field type in HubSpot, which requires:

- **Format:** Semicolon-separated string (not comma-separated)
- **Example:** `"Schichtplanung;Events;QR-Code"` (not `"Schichtplanung,Events,QR-Code"`)

### Allowed Values

All 13 values are allowed in HubSpot:

**Allowed Values:**

- Events
- QR-Code
- Schichtplanung
- Zeiterfassung
- Payroll Plus
- Abwesenheiten
- Digitale Personalakte
- Dokumentenmanagement
- AI Agent
- Trinkgeld
- Personaldienstleister
- Hardware Terminals
- Checklisten

### Adding a New "Interessiert an" Option (Runbook)

When adding a new option to the Interested In feature grid:

1. **HubSpot:** Add the option in HubSpot (run `v2/scripts/hubspot/update-interested-in-options.php` with `HUBSPOT_API_TOKEN` set, or PATCH the property `interested_in__c` with the new option).
2. **Form UI:** Add the feature button and icon in `v2/components/event-form.php` (same structure as existing options).
3. **Backend:** Add the value to `$allowedValues` in `v2/api/event-lead-capture.php`.
4. **Verification and data:** Update `v2/scripts/verify-event-form.php` (expected options list) and `v2/data/hubspot-property-interested_in__c.json` (options array).
5. **Docs:** Update this doc and `EVENTS_FORM_SALES_GUIDE.md` wherever the option count or full list is mentioned.

### Value Processing

**Frontend (`event-form.js`):**

- Collects checkbox values as array
- Converts to comma-separated string: `"Schichtplanung,Events"`

**Backend (`event-lead-capture.php`):**

- Receives comma-separated string or array
- Validates against allowed values list (case-sensitive exact match)
- Filters out invalid values
- Converts to semicolon-separated format: `"Schichtplanung;Events"`
- Logs warnings for invalid values filtered out

**Validation:**

- Values must match HubSpot allowed values exactly (case-sensitive)
- Invalid values are filtered out with warning logged
- Empty field results in no `interested_in__c` field sent to HubSpot
- Whitespace is trimmed before validation

**Example Processing:**

```
Frontend Input: "Schichtplanung,Abwesenheiten,Events"
↓
Backend Processing:
  - Valid: Schichtplanung ✅
  - Valid: Abwesenheiten ✅
  - Valid: Events ✅
↓
HubSpot Format: "Schichtplanung;Abwesenheiten;Events"
```

## Owner Selection Flow

The form uses a **two-step flow** where team members select their name first before the lead capture form is displayed.

### Step 1: Owner Selection Screen

When the page loads, team members see a grid-based owner selection screen:

- **Layout**: Responsive grid (3 columns portrait, 4 columns landscape)
- **Touch Targets**: Minimum 80px height, optimized for tablet use
- **Title**: "Wer bist du?" (Who are you?)
- **Subtitle**: Instructions to select name before handing tablet to lead

**Owner Selection Behavior:**

1. **Manual Selection**: Team member clicks their name from the grid
2. **URL Parameter**: If `?owner=Name` is in URL, owner is auto-selected and selection screen is skipped
3. **localStorage Persistence**: Selected owner is stored per event ID for page refresh persistence

### Step 2: Form Display

After owner selection:

- Owner selection screen hides
- Form displays with "Change Owner" button in header
- Owner value is set in hidden input field
- Form is ready for lead to fill out

### Change Owner

Team members can change the selected owner at any time:

- **Button Location**: Top-right of form header
- **Button Text**: "Besitzer ändern" (Change Owner)
- **Behavior**: Returns to owner selection screen

### Owner Distribution via HubSpot Workflow

**Important:** Owners are NOT directly assigned to contacts. Instead, the owner name is set in `utm_medium__c` for HubSpot workflow-based distribution.

**How It Works:**

1. Team member selects their name (e.g., "Daniela", "Felix")
2. Form submission includes owner name
3. System sets `utm_medium__c` to owner name (sanitized for UTM compatibility)
4. HubSpot workflow reads `utm_medium__c` and distributes leads using configured logic (round robin, direct assignment, etc.)

**Benefits:**

- More flexible distribution logic (can be changed in HubSpot without code changes)
- Supports round robin, weighted distribution, or custom assignment rules
- Easier to manage and update distribution rules
- No need to maintain owner ID mappings in code

**HubSpot Workflow Setup:**

A HubSpot workflow must be configured to:

- Trigger on contact creation with `sign_up_type__c` = "Event Lead Capture"
- Read `utm_medium__c` value (owner name)
- Assign contact to appropriate team member based on `utm_medium__c`
- Can use round robin, direct assignment, or custom logic

**Available Owners (Event-Specific):**

**Intergastra 2026 (9 owners):**

- Barti
- Daniela
- David
- Felix
- Freddie
- Kathrin
- Lena
- Michael
- Quirin

**Internorga 2026 (13 owners):**

- Barti
- Constantin
- Daniela
- David
- Fabian
- Felix
- Freddie
- Jakob
- John
- Lena
- Michelle
- Muhammed
- Quirin

**Usage Scenarios:**

- **Single Tablet, Single Owner**: Use URL parameter `?owner=Felix` to skip selection screen
- **Shared Tablet, Multiple Owners**: Team members select from grid before each lead
- **Form Reset**: When "Add Another" is clicked, owner remains selected, form resets
- **Page Refresh**: Owner selection persists via localStorage (per event ID)

## Tablet Optimization

### Touch Targets

- **Minimum Size:** 44x44px (iOS standard)
- **Input Height:** Minimum 44px
- **Button Height:** 56px (submit), 44px (others)
- **Spacing:** Minimum 8px between touch targets

### Typography

- **Font Size:** Minimum 16px (prevents iOS zoom)
- **Line Height:** 1.5 for readability
- **Font Weight:** 600 for buttons, 400 for inputs

### Form Controls

- **Dropdowns:** Custom button-style (not native select) for better UX
- **Checkboxes:** Large touch targets with visual feedback
- **Inputs:** Full-width on mobile, max-width on tablet
- **Buttons:** Full-width on mobile, centered on tablet

## Offline Support

### Storage

- Uses `localStorage` for simple queue management
- Stores submissions with timestamp and status
- Maximum 10 submissions queued (oldest removed if quota exceeded)

### Sync Logic

1. **Detection:** `navigator.onLine` + periodic check (every 30s)
2. **Queue:** Submissions stored when offline
3. **Sync:** Automatic when connection restored
4. **Retry:** Up to 5 attempts per submission
5. **Cleanup:** Synced submissions removed from queue

### User Feedback

- Visual offline indicator shows when offline
- Displays count of pending submissions
- Shows sync status during sync process

## Business Card Scanning

### Camera Access

- Uses `getUserMedia()` API
- Requests back camera (`facingMode: 'environment'`)
- Optimized for tablet use
- Handles permission errors gracefully

**Camera Lifecycle Management:**

- **Automatic Cleanup:** Camera stream closes immediately after image capture
- **Battery Optimization:** Prevents battery drain by stopping camera when not needed
- **Lifecycle Handlers:** Camera stops on page visibility change, unload, or hide events
- **Error Handling:** Try-finally pattern ensures camera always closes, even on errors
- **State Verification:** Verifies all tracks are stopped and hardware resources released

**Safety Features:**

- Prevents multiple simultaneous camera streams
- Force stop mechanism for aggressive cleanup if needed
- Debug logging tracks camera state for troubleshooting
- Verification ensures camera is actually stopped after cleanup

### Auto-capture (focus + stability)

When the camera modal is open, the scanner can trigger capture automatically when the card is in focus and held steady, so users on large tablets do not need to tap the capture button.

**How it works:**

- **Warm-up delay:** Auto-capture does not start until a short delay (e.g. 2 s) after the camera opens (`AUTO_CAPTURE_WARMUP_MS`), so the user can position the card before any capture logic runs. This prevents triggering on the first sharp frame (e.g. empty desk or wall).
- **Sharpness detection:** Laplacian variance is computed on a small analysis region of the live video (320×240). Higher variance = sharper (in-focus) image; blurry frames have lower variance. A higher threshold (e.g. 550) ensures only clearly in-focus content triggers.
- **Center-weighted check:** The center region (middle 50% of the frame) must contribute a minimum fraction of the total sharpness (`AUTO_CAPTURE_CENTER_SHARPNESS_MIN_RATIO`), so that sharp content is in the middle (card-like) and not only at edges (e.g. keyboard, screen).
- **Stability requirement:** Capture triggers only after sharpness (and center ratio) stay above thresholds for several consecutive samples (e.g. 7 samples at 180 ms interval ≈ 1.26 s of stable focus), to avoid capturing while the user is still moving the card.
- **Implementation:** Pure JavaScript (Canvas `getImageData` + Laplacian kernel); no OpenCV or other libraries. Config constants in `v2/js/business-card-scanner.js`: `AUTO_CAPTURE_WARMUP_MS`, `AUTO_CAPTURE_SHARPNESS_THRESHOLD`, `AUTO_CAPTURE_STABILITY_FRAMES`, `AUTO_CAPTURE_INTERVAL_MS`, `AUTO_CAPTURE_CENTER_SHARPNESS_MIN_RATIO`.
- **Manual fallback:** The capture button remains visible and works as before; users can tap if auto-capture does not fire (e.g. lighting or card type).
- **Hint:** The modal shows "Halte die Karte ruhig – Aufnahme erfolgt automatisch" so users know to hold the card steady.

**Debug tuning:** Add `?ocr_auto_capture_debug=1` to the URL or set `localStorage.setItem('ocr_auto_capture_debug', '1')` to log sharpness, center ratio, and consecutive sharp-frame count to the console. If auto-capture fires too early, increase `AUTO_CAPTURE_WARMUP_MS` or `AUTO_CAPTURE_SHARPNESS_THRESHOLD`; if it never fires, decrease the threshold or check lighting and that the card is in the center of the frame.

### OCR Processing

**Current Status:** ✅ Integrated with Google Cloud Vision API

**Configuration:**

1. **Set API Key** (choose one method):
   - Environment variable: `GOOGLE_VISION_API_KEY=your-api-key`
   - PHP constant: `define('GOOGLE_VISION_API_KEY', 'your-api-key');`
   - Fallback: Uses `GOOGLE_MAPS_API_KEY` if same GCP project

2. **API Features:**
   - Uses `DOCUMENT_TEXT_DETECTION` feature for structured text extraction
   - Supports multiple languages (German, English, etc.) via language hints
   - High accuracy for structured documents like business cards
   - Cost: ~$1.50 per 1,000 images (first 1,000/month free)

3. **Image Requirements:**
   - **File Size**: Maximum 3MB (becomes ~4MB after base64 encoding)
   - **Base64 Encoded Size**: Maximum 4MB (Vision API limit)
   - **Image Dimensions**: Maximum 20 megapixels (width × height ≤ 20,000,000 pixels)
   - **Minimum Dimensions**: At least 100×100 pixels for OCR accuracy
   - **Supported Formats**: JPEG, PNG, WebP
   - **Image Validation**: Automatically validates dimensions and file size before API call

4. **Error Handling:**
   - Comprehensive error logging for debugging
   - User-friendly error messages
   - Falls back to manual entry on API errors
   - Validates video readiness before capture
   - Validates canvas dimensions before processing
   - Timeout handling (30 seconds for OCR, 2 seconds for canvas conversion)

5. **Debugging:**
   - Add `?debug=1` to URL for console logging
   - Test page available at `/v2/admin/test-ocr-endpoint.php?debug=1`
   - All errors logged to `v2/logs/ocr-business-card-[date].log`

6. **Video Capture Flow:**
   - Checks video `readyState >= 2` before capture
   - Validates video dimensions (`videoWidth > 0`, `videoHeight > 0`)
   - Validates canvas dimensions before `toBlob()`
   - Fallback to `toDataURL()` if `toBlob()` fails or times out
   - Prevents multiple simultaneous captures

### OCR Field Mapping

The OCR system uses multiple parsing strategies to extract contact information:

**Extracted Fields:**

- **Name** → `firstname` / `lastname` (handles German names, honorifics, compound surnames)
- **Email** → `email` (prefers business emails over generic domains)
- **Phone** → `phone` (normalized to E.164 format, supports German, US, and international formats)
- **Company** → `company` (recognizes German legal forms: GmbH, AG, UG, e.K., KG, OHG)
- **Job Title** → `jobtitle` (expanded German/English keywords, position-based extraction)

**Parsing Strategies:**

1. **Structured Parsing** (primary): Uses Vision API's structured response with blocks, paragraphs, and words with bounding boxes. Identifies fields by position (top-left = name, center = company, etc.)

2. **Line-by-Line Parsing** (fallback): Improved parsing that handles:
   - German address patterns (Straße, Str., Platz, Weg)
   - German postal codes (5 digits)
   - German name patterns (compound names, "von", "zu")
   - German company suffixes (GmbH, AG, UG, e.K., KG, OHG)
   - Expanded job title keywords (German and English)

3. **Pattern-Based Parsing** (fallback): Independent field extraction using regex patterns for each field type

**Confidence Scoring:**

- Each parsing strategy receives a confidence score (0-1)
- Results are merged intelligently, using the best field from the highest-confidence strategy
- Fields are validated (email format, phone normalization, name patterns)

**Data Validation & Cleaning:**

After merging results from all strategies, extracted data passes through `validateAndCleanOCRData()`:

- **Email Validation:** Format validation using `filter_var()`, OCR error correction, lowercase normalization
- **Phone Normalization:** Converts to E.164 format (+[country][number]), handles German formats
- **Name Cleaning:** Minimum 3 characters, proper case normalization, invalid character removal
- **Company Cleaning:** Legal form normalization (gmbh → GmbH), trailing punctuation removal
- **Job Title Cleaning:** Removes company references, contact info, normalizes whitespace

**Validation Statistics:**

The system logs detailed validation statistics including:

- Fields before/after validation
- Fields removed (invalid)
- Fields cleaned (modified)
- Confidence scores per strategy

See [Field Extraction Guide](../../ocr/FIELD_EXTRACTION_GUIDE.md) for complete details.

**German-Specific Features:**

- Handles German names with honorifics (Herr, Frau, Dr., Prof.)
- Recognizes German compound surnames and "von"/"zu" prefixes
- Supports German phone formats: `+49`, `0049`, `0XXX` with various separators
- Identifies German company legal forms
- Recognizes German job titles (Geschäftsführer, Hoteldirektor, etc.)
- Handles German addresses and postal codes

## Offline Functionality

### Overview

The form includes comprehensive offline support for reliable lead capture even without internet connectivity. Submissions are stored locally and automatically synced when connectivity is restored.

### Storage Architecture

**Primary Storage: IndexedDB**

- Database: `EventLeadCaptureDB`
- Object Store: `event_submissions`
- Indexes: `timestamp`, `status`, `event_id`, `hash`
- Capacity: Hundreds of MB (much larger than localStorage)

**Fallback Storage: localStorage**

- Used if IndexedDB unavailable (older browsers)
- Automatic migration from localStorage to IndexedDB on first load
- Storage key: `event_form_submissions`

**Storage Schema:**

```javascript
{
  id: string,              // Unique submission ID
  data: object,            // Form data (all fields)
  timestamp: number,       // Submission timestamp
  status: string,          // 'pending' | 'synced' | 'failed' | 'needs_attention'
  hash: string,           // Duplicate detection hash
  attempts: number,        // Retry attempt count
  event_id: string,       // Event identifier
  syncedAt: number,       // Timestamp when synced (if synced)
  lastError: string,      // Last error message (if failed)
  lastErrorType: string,  // Error category
  lastErrorTime: number,  // Timestamp of last error
  nextRetryAt: number     // Timestamp for next retry (if pending)
}
```

### Network Detection

**Improved Reliability:**

- Replaces unreliable `navigator.onLine` with actual fetch test
- Health check endpoint: `/v2/api/health-check.php`
- Timeout: 5 seconds
- Result caching: 10 seconds to avoid excessive requests

**Network Status Flow:**

1. Check `navigator.onLine` (fast but unreliable)
2. If `true`, perform actual fetch to health-check endpoint
3. Cache result for 10 seconds
4. Use cached result for subsequent checks

### Duplicate Detection

**Hash Generation:**

- Based on: `email + event_id + 5-minute time window`
- Prevents duplicate submissions within 5-minute window
- Merges duplicates (keeps most complete data)

**Duplicate Handling:**

- Check hash before queuing
- If duplicate found, merge with existing submission
- Keep most complete data from both submissions

### Data Validation

**Before Queuing:**

- Required fields: `firstname`, `lastname`, `email`, `company`, `customer_type__c`, `owner`
- Email format validation (regex)
- Phone format validation (if provided)
- Data sanitization (trim whitespace)

**Validation Errors:**

- Non-retryable (validation errors don't sync)
- User-friendly German error messages

### Sync Strategy

**Background Sync API (Preferred):**

- Registers sync event: `sync:event-submissions`
- Processes queue when device comes online (even if tab closed)
- Fallback to polling if Background Sync unavailable

**Polling Fallback:**

- Checks every 30 seconds if online
- Processes queue if pending submissions exist

**Batch Processing:**

- Processes 5 submissions at a time
- Prevents overwhelming server
- FIFO order (oldest first)

### Retry Strategy

**Exponential Backoff:**

- Base delay: `min(2^attempt * 1000ms, 30000ms)` (max 30 seconds)
- Jitter: ±20% random variation
- Error-specific delays:
  - Timeout errors: 1.5x multiplier
  - Server errors: 2x multiplier

**Retry Attempts:**

- Maximum: 10 attempts (increased from 5)
- After 10 failures: Mark as `needs_attention` (not abandoned)
- Manual retry available via UI button

**Error Categories:**

- `timeout`: Network timeout (retryable)
- `network`: Network error (retryable)
- `client_error`: 4xx errors (non-retryable)
- `server_error`: 5xx errors (retryable)
- `unknown`: Unknown error (retryable)

### Storage Management

**Automatic Cleanup:**

- Synced submissions: Deleted after 7 days
- Storage limit: Maximum 100 submissions (delete oldest if exceeded)
- Failed submissions: Kept indefinitely (for manual retry)

**Cleanup Triggers:**

- After each sync cycle
- On initialization
- Before adding new submission (if storage full)

### User Interface

**Offline Indicator:**

- Shows when offline or when queue has pending/failed submissions
- Displays counts: pending, failed, needs attention
- Manual retry button (shown when failed submissions exist)
- Progress bar (future enhancement)

**Status Messages:**

- Offline: "Offline - Formulare werden gespeichert und später gesendet"
- Pending: "X Formulare wartet"
- Failed: "X fehlgeschlagen"
- Needs Attention: "X benötigt Aufmerksamkeit"

### Error Handling

**User-Friendly Messages (German):**

- Timeout: "Zeitüberschreitung. Bitte prüfe deine Internetverbindung."
- Network: "Netzwerkfehler. Bitte prüfe deine Internetverbindung."
- Client Error: "Ungültige Daten. Bitte überprüfe deine Eingaben."
- Server Error: "Serverfehler. Bitte versuche es später erneut."

**Error Logging:**

- All operations logged (if debug mode enabled)
- Logs stored locally (future: sync to server)
- Includes: timestamp, error type, submission ID, attempt count

### Testing Offline Functionality

**Manual Testing Checklist:**

- [ ] Submit form while offline (airplane mode)
- [ ] Verify submission appears in queue
- [ ] Go online and verify automatic sync
- [ ] Test with slow 3G connection
- [ ] Test with intermittent connectivity
- [ ] Test storage quota exceeded scenario
- [ ] Test multiple rapid submissions
- [ ] Test sync after browser restart
- [ ] Test duplicate detection
- [ ] Test manual retry button

**Browser Compatibility:**

- iPad Safari: Full support (IndexedDB + Background Sync)
- Android Chrome: Full support (IndexedDB + Background Sync)
- Older browsers: Falls back to localStorage + polling

### Troubleshooting

**Common Issues:**

1. **Submissions not syncing:**
   - Check network status (health check endpoint)
   - Verify Background Sync API support
   - Check browser console for errors
   - Try manual retry button

2. **Storage quota exceeded:**
   - Automatic cleanup should handle this
   - Oldest synced submissions removed first
   - Failed submissions kept for manual retry

3. **Duplicate submissions:**
   - Check hash generation logic (offline sync)
   - Verify 5-minute time window (API endpoint)
   - Check merge logic (offline sync)
   - Verify API duplicate detection is working (check logs)
   - Check frontend timestamp tracking (2-second minimum)

**Debug Mode:**

- Add `?debug=1` to URL for detailed console logging
- Logs all offline operations, sync attempts, errors

## Performance Targets

- **Form Load:** <1s
- **Form Submission:** <2s
- **OCR Processing:** <5s
- **PageSpeed Score:** >90 (mobile and desktop)
- **LCP:** <2.5s
- **FID:** <100ms
- **CLS:** <0.1

## Testing

### Functional Testing

- [ ] Form submission with all field combinations
- [ ] Business card scanning on actual tablet device
- [ ] OCR accuracy with various business card formats
- [ ] Offline submission and sync functionality
- [ ] Owner assignment with different owners
- [ ] Duplicate detection logic
- [ ] Error handling (network errors, API errors, validation errors)

### Device Testing

- [ ] iPad (various sizes: 9.7", 10.2", 11", 12.9")
- [ ] Android tablets (various sizes)
- [ ] Mobile phones (iPhone, Android)
- [ ] Touch target sizes (verify 44px minimum)
- [ ] Form usability with touch input
- [ ] Camera access on different devices/browsers

### Performance Testing

- [ ] PageSpeed Insights (target: >90 mobile, >90 desktop)
- [ ] LCP (target: <2.5s)
- [ ] FID (target: <100ms)
- [ ] CLS (target: <0.1)
- [ ] Form submission speed (target: <2s)
- [ ] OCR processing speed (target: <5s)

### Accessibility Testing

- [ ] Keyboard navigation
- [ ] Screen reader compatibility
- [ ] Focus management
- [ ] Color contrast ratios
- [ ] Form labels and ARIA attributes

### Browser Testing

- [ ] Safari (iOS/iPadOS)
- [ ] Chrome (Android/Desktop)
- [ ] Firefox
- [ ] Camera API compatibility across browsers
- [ ] Offline detection across browsers

## Troubleshooting

### Production Deployment Checklist

**Before deploying to production, verify:**

1. **Response Headers:**
   - Content-Type: `application/json; charset=utf-8`
   - X-Content-Type-Options: `nosniff`
   - Cache-Control: `no-cache, must-revalidate`

2. **Test API Response:**

   ```bash
   php v2/scripts/test-production-submission.php https://www.ordio.com
   ```

3. **Verify Diagnostics:**
   ```bash
   curl "https://www.ordio.com/v2/api/event-lead-capture-diagnostics.php?password=elc-diagnostic-2026&test=true"
   ```

**CRITICAL: Header Placement Pattern**

**ALWAYS set headers AFTER includes** (like working APIs: `collect-lead.php`, `submit-template.php`):

```php
// ✅ CORRECT: Includes FIRST, headers AFTER
require_once __DIR__ . '/../config/hubspot-config.php';
require_once __DIR__ . '/../config/hubspot-api-helpers.php';
require_once __DIR__ . '/../helpers/logger.php';

// THEN set headers
header('Content-Type: application/json; charset=utf-8');
header('X-Content-Type-Options: nosniff');
header('Cache-Control: no-cache, must-revalidate');
```

**❌ WRONG: Headers before includes** (causes "headers already sent" errors if includes output anything):

```php
// ❌ WRONG: Headers before includes
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/../config/hubspot-config.php'; // May output whitespace/errors
```

**Why:** If any included file outputs anything (whitespace, BOM, errors), headers fail silently or cause CORB errors in production.

### CORB (Cross-Origin Read Blocking) Errors

**Symptoms:**

- Browser DevTools shows "Response was blocked by CORB"
- Form shows success but data doesn't reach HubSpot
- JSON parsing errors in console

**Root Causes:**

1. **Headers set BEFORE includes** - If includes output anything, headers fail silently
2. Missing or incorrect Content-Type header
3. Missing X-Content-Type-Options header
4. Response contains HTML/XML instead of pure JSON
5. Output buffering issues causing whitespace before JSON

**Solutions:**

1. **Verify Response Headers:**
   - Open DevTools → Network tab
   - Submit form
   - Check response headers for `/v2/api/event-lead-capture.php`
   - Verify headers match requirements above

2. **Check for Output Before JSON:**
   - Look for any `echo`, `print`, or whitespace before JSON
   - Ensure `ob_end_clean()` is called before JSON output
   - Check for PHP warnings/errors in response

3. **Test JSON Validity:**

   ```bash
   php v2/scripts/test-production-submission.php
   ```

4. **Verify No HTML/XML in Response:**
   - Response should be pure JSON
   - No HTML tags, no XML, no PHP errors

**Prevention:**

- Always use `outputJsonResponse()` helper function
- Never output anything before JSON
- Always set headers before output
- Use output buffering cleanup

### HubSpot Submission Not Working (Success Page Shows But Data Doesn't Reach HubSpot)

**Symptoms:**

- Form shows success page after submission
- All fields appear filled correctly
- OCR works and fills form fields
- But data doesn't appear in HubSpot contacts

**Root Causes:**

1. API returns success even when HubSpot call fails
2. Silent failures in HubSpot Forms API v3 call
3. Configuration issues (form GUID, portal ID, API token)
4. Response parsing issues
5. Network/connectivity issues

**Diagnostic Steps:**

1. **Check Diagnostics Endpoint:**

   ```bash
   # Basic diagnostics
   curl "https://www.ordio.com/v2/api/event-lead-capture-diagnostics.php?password=elc-diagnostic-2026"

   # With test submission
   curl "https://www.ordio.com/v2/api/event-lead-capture-diagnostics.php?password=elc-diagnostic-2026&test=true"
   ```

2. **Run Test Script:**

   ```bash
   php v2/scripts/test-hubspot-submission.php
   ```

   This will:
   - Verify configuration (form GUID, portal ID, API token)
   - Test API connectivity
   - Submit test data to HubSpot
   - Show detailed results

3. **Check Browser Console (Development Mode):**
   - Open browser DevTools → Console
   - Submit form
   - Look for `[EventForm]` log messages:
     - `[EventForm] Submitting form data:` - Shows what's being sent
     - `[EventForm] API response status:` - Shows HTTP status
     - `[EventForm] API response:` - Shows full response
     - `[EventForm] Success verification:` - Shows success indicators

4. **Check Network Tab:**
   - Open browser DevTools → Network
   - Submit form
   - Find request to `/v2/api/event-lead-capture.php`
   - Check:
     - **Request Payload:** Verify all fields are included
     - **Response Status:** Should be 200
     - **Response Body:** Check `success` field and any error messages

5. **Check Backend Logs:**

   ```bash
   # Check today's log file
   tail -f v2/logs/event-lead-capture-$(date +%Y-%m-%d).log

   # Or check all recent logs
   ls -lt v2/logs/event-lead-capture-*.log | head -5
   ```

   Look for:
   - `Event lead capture submission attempt` - Shows submission started
   - `HubSpot API call completed` - Shows API call result
   - `Event lead capture submission successful` - Shows success
   - `Event lead capture submission failed` - Shows failure with details

6. **Verify HubSpot Configuration:**
   - **Form GUID:** Should be `e9d5fd47-6772-4c0a-943b-8f726afd8e4b`
   - **Portal ID:** Should be `145133546`
   - **API Token:** Should start with `pat-eu1-` or `pat-na1-`
   - **Forms API URL:** Should be `https://api.hsforms.com/submissions/v3/integration/secure/submit/145133546/e9d5fd47-6772-4c0a-943b-8f726afd8e4b`

**Common Issues & Solutions:**

1. **HTTP 401 (Unauthorized):**
   - **Cause:** API token invalid or expired
   - **Solution:** Verify `HUBSPOT_API_TOKEN` environment variable or config file
   - **Check:** Run diagnostics endpoint to verify token

2. **HTTP 403 (Forbidden):**
   - **Cause:** API token lacks required permissions
   - **Solution:** Verify token has scopes: `crm.objects.contacts.write`, `forms-uploaded-forms`
   - **Check:** HubSpot account settings → Integrations → Private Apps

3. **HTTP 404 (Not Found):**
   - **Cause:** Form GUID or Portal ID incorrect
   - **Solution:** Verify form exists in HubSpot and GUID matches
   - **Check:** HubSpot → Marketing → Forms → Find form → Copy GUID

4. **HTTP 400 (Bad Request):**
   - **Cause:** Payload structure incorrect or field names don't match
   - **Solution:** Check field names match HubSpot property names exactly
   - **Check:** Response body will show specific field errors

5. **HTTP 200 but No Contact Created:**
   - **Cause:** Response has errors in body even though HTTP code is 200
   - **Solution:** Check response body for error messages
   - **Check:** Enhanced logging now captures this - check logs

6. **Network Errors:**
   - **Cause:** Connectivity issues, timeouts, firewall blocking
   - **Solution:** Check network connectivity, firewall rules
   - **Check:** Test script will show network errors

**Prevention:**

- Always test with `test-hubspot-submission.php` after configuration changes
- Monitor logs regularly for submission failures
- Use diagnostics endpoint to verify configuration
- Test form submission in browser DevTools before events

**Debugging Tools:**

- **Diagnostics Endpoint:** `/v2/api/event-lead-capture-diagnostics.php?password=elc-diagnostic-2026`
- **Test Script:** `v2/scripts/test-hubspot-submission.php`
- **Browser Console:** Development mode logging (localhost only)
- **Backend Logs:** `v2/logs/event-lead-capture-YYYY-MM-DD.log`

### Form Submission Failing with "Bitte fülle alle erforderlichen Felder aus"

**Symptoms:**

- Form shows error "Bitte fülle alle erforderlichen Felder aus" even when fields appear filled
- Error occurs immediately after clicking submit button
- Fields show green checkmarks but submission still fails

**Root Causes:**

1. Validation mismatch between frontend and backend
2. Hidden inputs (dropdowns) not being collected properly
3. Form stuck in disabled state
4. Required fields not properly validated

**Solutions:**

1. **Check Browser Console (Development Mode):**
   - Look for `[EventForm]` log messages
   - Check for "Missing required fields" warnings
   - Verify form data collection logs show all required fields

2. **Verify All Required Fields:**
   - Email (visible input)
   - First name (visible input)
   - Last name (visible input)
   - Company (visible input)
   - Customer type (hidden input from dropdown)
   - Owner (hidden input)

3. **Check Form State:**
   - Ensure form container doesn't have `pointer-events: none`
   - Ensure inputs are not disabled
   - Check browser DevTools → Elements → Inspect form container

4. **Verify Hidden Inputs:**
   - Open browser DevTools → Elements
   - Find hidden inputs: `#customer_type`, `#owner`
   - Verify they have values (not empty)

5. **Run Test Script:**

   ```bash
   php v2/scripts/test-event-form-submission-debug.php
   ```

6. **Check Backend Logs:**
   - Check `logs/` directory for API errors
   - Look for "Missing required fields" errors
   - Verify which fields backend reports as missing

**Prevention:**

- Always test form submission after making changes
- Use browser DevTools to verify form data collection
- Check console logs in development mode
- Run test script before deploying changes

### Dropdowns Not Working After Form Reset

**Symptoms:**

- After successful submission, dropdown menus don't respond to clicks
- Dropdowns work initially but stop working after "Add Another" or changing owner
- Form shows but dropdowns are unresponsive

**Root Causes:**

1. Stale dropdowns array accumulating references
2. Duplicate event listeners from re-initialization
3. Old event listeners not removed before adding new ones

**Solutions:**

1. **Verify dropdown initialization:**
   - Check browser console for JavaScript errors
   - Verify dropdowns are being re-initialized after form reset
   - Check that `this.dropdowns` array is cleared before re-initialization

2. **Check event listeners:**
   - Use browser DevTools → Elements → Event Listeners
   - Verify no duplicate click/keydown listeners on dropdown buttons
   - Check that dropdown elements are properly cloned when re-initializing

3. **Verify initialization flags:**
   - Check that `dropdownInitialized` flags are cleared before re-initialization
   - Verify `setupDropdowns()` is called after form reset

4. **Test form reset:**
   - Submit form successfully
   - Click "Add Another" or change owner
   - Verify dropdowns work correctly
   - Test multiple submissions in a row

**Prevention:**

- Always clear `this.dropdowns` array before re-initialization
- Clone dropdown elements to remove old event listeners
- Ensure fresh initialization each time form is shown

### Validation States Not Cleared After Form Reset

**Symptoms:**

- After form submission and reset, fields show green checkmarks (validation success indicators)
- Validation states persist even though fields are empty
- Success icons remain visible after "Add Another" or changing owner

**Root Causes:**

1. `resetForm()` method only cleared `.error` class but not `.valid` class
2. Success icons (`.field-success-icon`) were not removed during reset
3. `aria-invalid` attributes were not cleared

**Solutions:**

1. **Verify validation state clearing:**
   - Check that `resetForm()` removes both `.error` and `.valid` classes
   - Verify success icons are removed from DOM
   - Check that `aria-invalid` attributes are cleared
   - Use browser DevTools → Elements → Inspect form fields after reset

2. **Test form reset:**
   - Fill form with valid data (trigger validation states)
   - Submit form successfully
   - Click "Add Another" or change owner
   - Verify no green checkmarks or success icons remain
   - Verify all fields are empty and show no validation states

3. **Use testing script:**
   - Load `v2/scripts/test-event-form-validation-reset.js` in browser console
   - Run: `testEventFormValidationReset()`
   - Review test results for any failures

**Prevention:**

- Always clear both error and success validation states in `resetForm()`
- Remove success icons from DOM during reset
- Clear accessibility attributes (`aria-invalid`)
- Test form reset flow after any validation changes

### Error Message Flashing Before Success

**Symptoms:**

- Error message appears briefly under form before success screen shows
- Error state flashes during successful submissions
- Visual glitch during form submission
- Field-level error messages visible during successful submission

**Root Causes:**

1. Error state hidden too late in submission flow
2. Error message text not cleared, causing visual artifacts
3. Field-level error messages (`.form-error`) not cleared before submission
4. Timing issues allowing error state to flash
5. Error state could be shown even when success state is visible (race condition)

**Solutions:**

1. **Verify error state hiding:**
   - Check that `hideErrorState()` is called at start of submission
   - Verify error message text is cleared
   - Check browser DevTools → Elements → Check error state display property
   - Verify all field-level errors (`.form-error`) are cleared

2. **Check submission flow:**
   - Verify error state is hidden before validation
   - Verify error state is hidden before async operations
   - Check that `handleSuccess()` hides error state
   - Verify field errors are cleared at multiple points:
     - At start of submission handler
     - At start of `submitForm()` method
     - Right before API call
     - In `handleSuccess()` before showing success

3. **Test successful submissions:**
   - Submit form with all fields filled
   - Verify no error flash before success screen
   - Test rapid submissions to ensure no flashing
   - Verify no field-level errors appear during successful submission

4. **Code fixes applied:**
   - Added clearing of all `.form-error` elements before submission
   - Added removal of `.error` classes from all fields before submission
   - Updated `showErrorState()` to check if success state is visible (prevents race conditions)
   - Multiple defensive checks at every step of submission flow

**Prevention:**

- Hide error state immediately at start of submission
- Clear error message text to prevent visual artifacts
- Clear ALL field-level error messages before submission
- Remove error classes from all fields before submission
- Prevent `showErrorState()` from showing errors when success is visible
- Add multiple defensive checks at every step of submission flow
- Test form submission flow after any validation or error handling changes

### Form Not Submitting

1. Check browser console for JavaScript errors
2. Verify API endpoint is accessible
3. Check HubSpot API token configuration
4. Review API logs: `v2/logs/event-lead-capture-YYYY-MM-DD.log`

### Owner Not Assigned

1. Verify `utm_medium__c` contains the correct owner name in HubSpot
2. Check that HubSpot workflow is configured to read `utm_medium__c` for distribution
3. Verify workflow is active and running correctly
4. Check API logs for owner name being set to `utm_medium__c`
5. Contact HubSpot admin if workflow needs to be set up or updated

### OCR Button Not Visible

**Symptoms:**

- OCR button ("Visitenkarte scannen") doesn't appear after form reset
- Button missing when clicking "Add Another" or changing owner
- Button works initially but disappears for subsequent lead submissions

**Root Causes:**

1. Camera scan section starts hidden (`display: none`) in HTML
2. Scanner initializes before form is visible, may not find button
3. Scanner doesn't re-check button availability after form reset
4. Camera section visibility not explicitly ensured in `resetForm()`

**Solutions:**

1. **Verify camera section visibility:**
   - Check that `ensureCameraSectionVisible()` is called in `showForm()` and `resetForm()`
   - Verify camera section has `display: block` when form is shown
   - Use browser DevTools → Elements → Check `#camera-scan-section` display property

2. **Check scanner initialization:**
   - Verify `window.businessCardScanner` exists
   - Check that `ensureInitialized()` is called after form is shown
   - Review console for scanner initialization messages (debug mode)

3. **Test form reset:**
   - Submit form successfully
   - Click "Add Another" or change owner
   - Verify OCR button appears
   - Test scanner functionality

4. **Code fixes applied:**
   - Added `ensureCameraSectionVisible()` helper method
   - Camera section explicitly shown in `resetForm()` before `showForm()`
   - Scanner re-initialization called in both `showForm()` and `resetForm()`
   - Scanner handles delayed initialization gracefully

**Prevention:**

- Always ensure camera section visibility when showing form
- Call scanner re-initialization after form operations
- Test OCR button visibility after form reset
- Verify scanner works for each new lead submission

### OCR Not Working

1. Verify camera permissions granted
2. Check browser support for `getUserMedia()`
3. Verify OCR endpoint is accessible (`/v2/api/ocr-business-card.php`)
4. Check Google Cloud Vision API key configuration
5. Review OCR logs: `v2/logs/ocr-business-card-YYYY-MM-DD.log`
6. Enable debug mode: Add `?debug=1` to URL for detailed logging
7. Test OCR endpoint directly: `/v2/admin/test-ocr-endpoint.php?debug=1`
8. Verify OCR button is visible (see "OCR Button Not Visible" section above)

### Phone Field Stays Green When Empty

**Symptoms:**

- Phone field shows green checkmark when valid number is entered
- After deleting phone number, green checkmark remains visible
- Field appears validated even though it's empty

**Root Causes:**

1. `validatePhone()` returns `true` for empty phone (optional field) but doesn't clear success state
2. Phone input event listener doesn't clear success state when field becomes empty
3. `clearFieldError()` explicitly doesn't remove `.valid` class (by design for other fields)

**Solutions:**

1. **Updated `validatePhone()` method:**
   - Clears success state when phone is empty
   - Shows hint for optional field
   - Returns `true` but without success indicator

2. **Added `clearFieldSuccess()` helper method:**
   - Removes `.valid` class
   - Removes success icon
   - Clears `aria-invalid` attribute

3. **Updated phone input event listener:**
   - Clears success state when phone becomes empty after formatting
   - Shows hint when field is empty

**Prevention:**

- Always clear success state for optional fields when they become empty
- Add input event listeners to clear success state on deletion
- Test validation state clearing for all field types

### Form Validation Too Strict

**Symptoms:**

- Form shows "required" errors for fields that have content
- Form won't submit with reasonable data
- Validation blocks submission even with valid-looking data

**Root Causes:**

1. Validation uses `.trim()` checks which reject whitespace-only values
2. Email validation too strict (complex regex)
3. Phone validation too strict (enforces exact format)
4. Form data collection check rejects whitespace-only values

**Solutions:**

1. **Relaxed form data collection:**
   - Removed `.trim()` checks - backend handles trimming
   - Only check for `null`, `undefined`, or empty string `""`
   - Allow whitespace-only values (monitoring will catch)

2. **Relaxed `validateForm()` method:**
   - Only check for completely empty (not whitespace)
   - Allow minimal values (single characters acceptable)
   - Focus on preventing empty submissions

3. **Relaxed email validation:**
   - Simple format check: just `@` and `.` presence
   - Removed complex regex
   - Basic validation only - backend handles final check

4. **Relaxed phone validation:**
   - Accept any reasonable phone format
   - Don't enforce strict length requirements
   - Allow incomplete numbers (monitoring will catch)

5. **Fixed customer_type validation:**
   - Auto-set default value "Neukunde" if empty
   - Don't show error if default value exists

**Prevention:**

- Use less strict validation when monitoring is available
- Focus on preventing empty submissions, not perfect format
- Let backend handle final validation and trimming
- Test with minimal data to ensure form accepts reasonable input

### OCR Not Extracting Fields Correctly

1. **Check raw OCR text**: Enable debug mode to see full text extracted from image
2. **Review parsing strategies**: Check which strategy was used and its confidence score
3. **Verify business card format**: Ensure card matches supported layouts (traditional, modern, vertical)
4. **Test with test suite**: Run `php v2/scripts/test-ocr-parsing.php` to verify parsing logic
5. **Check German content**: Verify German names, addresses, phone formats are recognized
6. **Review confidence scores**: Low confidence may indicate parsing issues
7. **Check for OCR errors**: Common issues include misread characters, poor image quality, complex layouts

### Image Size or Dimension Errors

1. **File too large**: Maximum file size is 3MB (becomes ~4MB after base64 encoding)
   - Solution: Compress or resize image before uploading
   - Check: File size shown in error message

2. **Image dimensions too large**: Maximum 20 megapixels (width × height ≤ 20,000,000)
   - Solution: Resize image to smaller dimensions
   - Check: Dimensions shown in error message

3. **Image dimensions too small**: Minimum 100×100 pixels for OCR accuracy
   - Solution: Use higher resolution image
   - Check: Dimensions shown in error message

4. **Base64 encoding too large**: Maximum 4MB after encoding
   - Solution: Reduce image file size or dimensions
   - Check: Estimated base64 size in error message

### Offline Sync Not Working

1. Verify `localStorage` is available
2. Check `navigator.onLine` detection
3. Review sync queue in browser DevTools → Application → Local Storage
4. Check sync logs in browser console

## Configuration

### HubSpot Form GUID

Defined in `v2/config/hubspot-config.php`:

```php
define('HUBSPOT_FORM_GUID_EVENT_LEAD_CAPTURE', 'e9d5fd47-6772-4c0a-943b-8f726afd8e4b');
```

### Owner Name Configuration

Owner names are set in `v2/components/event-form.php` and `v2/components/owner-selection-screen.php` in the `$eventOwners` array. These names are automatically sanitized and set to `utm_medium__c` for HubSpot workflow distribution.

**No owner ID mapping needed** - HubSpot workflow handles owner assignment based on `utm_medium__c` value.

To add or update owners for an event, update the `$eventOwners` array in both component files:

```php
$eventOwners = [
    'intergastra-2026' => [
        'Barti',
        'Daniela',
        // ... etc
    ],
    'internorga-2026' => [
        // ... etc
    ]
];
```

### OCR Integration

**Current Implementation:** ✅ Fully integrated with Google Cloud Vision API

**Configuration:**

- API key: Configured in `v2/config/google-vision.php`
- Priority order: Environment variable `GOOGLE_VISION_API_KEY` → Constant `GOOGLE_VISION_API_KEY` → Google Maps API key from project ordio-256916
- Language hints: German (`de`) and English (`en`) configured for better accuracy
- Detection mode: `DOCUMENT_TEXT_DETECTION` for structured business card parsing

**Setup Requirements:**

1. **Enable Vision API**: Visit https://console.cloud.google.com/apis/library/vision.googleapis.com?project=ordio-256916
2. **Enable Billing**: Visit https://console.cloud.google.com/billing?project=ordio-256916 (required for Vision API)
3. **Wait for Propagation**: After enabling, wait a few minutes for changes to propagate

**For complete setup instructions, see:** [`docs/systems/ocr/GOOGLE_VISION_API_SETUP.md`](../../ocr/GOOGLE_VISION_API_SETUP.md)

**Quick Diagnostics:**

- Run: `php v2/scripts/ocr-diagnose-api-key.php`
- Test API: `php v2/scripts/test-vision-api-direct.php`
- Web dashboard: `/v2/admin/ocr-diagnostics.php?debug=1`

**Image Requirements:**

- **File Size**: Maximum 3MB (becomes ~4MB after base64 encoding)
- **Base64 Encoded Size**: Maximum 4MB (Vision API limit)
- **Image Dimensions**: Maximum 20 megapixels (width × height ≤ 20,000,000 pixels)
- **Minimum Dimensions**: At least 100×100 pixels for OCR accuracy
- **Supported Formats**: JPEG, PNG, WebP

**Parsing Features:**

- Multi-strategy parsing (structured, line-by-line, pattern-based)
- Confidence scoring and intelligent result merging
- German-specific patterns (names, addresses, phone formats, company types)
- Support for various business card layouts

**Testing:**

- Test script: `php v2/scripts/test-ocr-parsing.php` (tests parsing logic with sample text)
- Test with image: `php v2/scripts/test-ocr-with-image.php <image_path>` (tests full OCR flow)
- Test endpoint: `/v2/admin/test-ocr-endpoint.php?debug=1` (web interface)
- Debug mode: Add `?debug=1` to URL for detailed logging

## Validation Testing

### Email Validation

**Logic:**

- Required field validation (shows error if empty)
- RFC 5322 compliant regex pattern
- Additional check: must have at least one dot after `@`
- Shows success indicator (green checkmark) when valid

**Test Cases:**

- Empty required field → Error shown
- Invalid formats (no `@`, no dot, etc.) → Error shown
- Valid formats → Success shown
- Edge cases: `test@domain`, `test@domain.`, `test@domain.co.uk`

**Testing Script:**

```bash
python3 v2/scripts/test-event-form-validation.py
```

### Phone Validation

**Logic (updated 2026-02-08):**

- Optional field (no error if empty, shows hint instead)
- **Digit range:** 7–20 digits (DACH + buffer; E.164 max is 15)
- **Formats:** +49 (DE), +43 (AT), +41 (CH), periods, dashes, parentheses, 00 prefix
- **Success:** 7–20 digits → green checkmark, hint hidden
- **Hint only:** < 7 or > 20 digits → hint shown, submit allowed (lenient)
- **Backend:** Optional validation when provided; rejects invalid (7–20 digits)

**Test Cases:**

- Empty field → Hint shown, no error
- Valid German: `+49 123 456789`, `+49.30.12345678` → Validates
- Valid DACH: `+43 1 1234567890`, `+41 44 123 45 67` → Validates
- 15–20 digits → Validates
- < 7 digits, > 20 digits → Hint shown, submit allowed

**Testing Script:**

```bash
python3 v2/scripts/test-event-form-validation.py
```

### Form Reset Testing

**Browser Testing Script:**

- Load `v2/scripts/test-event-form-validation-reset.js` in browser console
- Run: `testEventFormValidationReset()`
- Tests:
  - Validation states are cleared after reset
  - No `.valid` classes remain
  - No success icons remain
  - All fields are empty
  - Dropdowns are functional

**Manual Testing Checklist:**

1. Fill form with valid data (trigger validation)
2. Submit form successfully
3. Click "Add Another" or change owner
4. Verify:
   - All fields are empty
   - No green checkmarks visible
   - No validation states shown
   - Dropdowns work correctly
   - Can submit new form

## Related Documentation

- [HubSpot Integration Guide](../../guides/HUBSPOT_INTEGRATION_GUIDE.md)
- [Form Configuration Reference](FORM_CONFIGURATION_REFERENCE.md)
- [Form to Page Mapping](FORM_TO_PAGE_MAPPING.md)
- [Event Form Verification](EVENT_FORM_VERIFICATION.md)
