# CRM Conventions

> CRM-specific constraints derived from CLAUDE.md and project patterns.
> Always applies when implementing any CoreApp or CrmApp code.

---

## PHP Conventions

### ModelFilter (mandatory for all filterable models)
```php
// Every model with a list page uses EloquentFilter\Filterable
use EloquentFilter\Filterable;

class Lead extends Model
{
    use Filterable, SoftDeletes, HasCustomFields, HasPipeline,
        HasActivityTimeline, FiresWorkflowEvents;

    public function modelFilter(): string { return LeadFilter::class; }
}

// Service baseQuery = one line only
public function baseQuery(array $params)
{
    return Lead::filter($params);
}
```

### Mutations always redirect()
```php
// ✅ Correct
return redirect()->route('leads.index')->with('success', 'Lead updated.');

// ❌ Wrong — breaks Inertia
return response()->json(['status' => 'success']);
```
Exception: endpoints called via `axios`/`fetch` directly (search, file upload, `wantsJson()` on index).

### Bulk-action route before resource
```php
Route::post('leads/bulk-action', [LeadController::class, 'bulkAction'])->name('leads.bulk-action');
Route::resource('leads', LeadController::class)->names('leads');
```

### Status enums — always use enum values
```php
// ✅ Use enum values
LeadStatusEnum::CONVERTED->value   // e.g. 43
// ❌ Never hardcode integers
['status' => 43]  // what is 43?
```

### Central DB explicit
```php
// Tenant DB (default) — standard Eloquent
Lead::create([...]);
// Central DB — explicit connection
\DB::connection('central')->table('app_managements')->...
```

---

## Frontend Conventions

### Page layout (mandatory pattern)
```tsx
// ✅ Always use Page.layout — never wrap AppLayout in render body
function Index() {
    return <><Head title="Leads" />{/* content */}</>;
}
Index.layout = (page: ReactNode) => (
    <AppLayout title="Leads" breadcrumbs={[
        { title: 'Home', href: '/' },
        { title: 'CRM', href: '#' },
        { title: 'Leads', href: '#' },
    ]}>
        {page}
    </AppLayout>
);
```

### Page content wrapper (mandatory)
```tsx
<div className="no-scrollbar rounded-xl bg-gray-100/55 p-2 sm:p-4">
    {/* header, stat cards, datatable */}
</div>
```

### Page subtitle format
```tsx
<div className="flex items-center text-sm text-gray-600">
    <span>CRM</span>
    <span className="mx-2">›</span>
    <span>Leads</span>
</div>
```

### route() helper declaration
```tsx
// At top of file — never import from ziggy-js
declare const route: (...args: any[]) => string;
```

### Label rebranding
```tsx
// In any CRM page that shows entity names
import { useLabel } from '@/contexts/label-context';

const leadLabel = useLabel('lead');  // { singular: 'Application', plural: 'Applications' }
// Use leadLabel.plural in headings, leadLabel.singular in forms
```

### DynamicFields component
```tsx
// Render custom fields in Create/Edit forms
import DynamicFields from '@/components/crm/dynamic-fields';

<DynamicFields fields={customFields} values={data.custom_fields}
    onChange={(slug, val) => setData('custom_fields', {...data.custom_fields, [slug]: val})} />
```

---

## CRM-Specific Rules

| Rule | Reason |
|------|--------|
| `LeadFilter` / `DealFilter` — no joins on `custom_field_values` for searchable fields | R1 performance |
| `LeadService::convert()` — DB transaction, event fires after commit | R3 safety |
| `FeaturePackSeeder` — never calls delete() or truncate() | R4 additive-only |
| `WorkflowDispatcher` — static `$running` guard before every dispatch | R5 loop prevention |
| Default pipeline on create — `Pipeline::where('is_default',true)->firstOrFail()` | throws `PipelineNotConfiguredException` if pack not seeded |
| `LeadPolicy::view()` — admin=true, manager checks branch_id, else checks owner_id | authorization scope |
