# Trakyo - Dolibarr Contacts Integration Plan

## Goal

Integrate Dolibarr clients and contacts into Trakyo so users can select business contacts inside Affaires, Projects, and future modules.

Dolibarr remains the source of truth. Trakyo stores a local synchronized copy for fast search, filtering, autocomplete, maps, and relations.

---

## Context

Trakyo needs a `BusinessContact` system.

Current expected data:

- A business contact can be a Dolibarr third party / tiers.
- A business contact can also be a physical contact linked to a Dolibarr tiers.
- Contact data may include:
  - ID
  - name
  - lastname
  - civility
  - poste
  - address
  - tel
  - email
  - latitude
  - longitude
  - type

Dolibarr version target: `19.0.2`.

---

## Recommended Architecture

Use one-way synchronization first:

```text
Dolibarr -> Trakyo
```

Do not use live Dolibarr API calls inside normal Trakyo pages.

Instead:

1. Admin configures Dolibarr API access.
2. Admin clicks sync, or sync runs by queue/schedule.
3. Dolibarr clients and contacts are copied into Trakyo database.
4. Trakyo UI uses local `business_contacts` table.
5. Dolibarr IDs are preserved for future resync/update.

---

## Dolibarr Data Sources

### 1. Thirdparties / Tiers

Used for client companies.

Example endpoint:

```http
GET /api/index.php/thirdparties?mode=1&limit=100&page=0
```

`mode=1` means customers/clients.

### 2. Contacts

Used for physical people linked to companies.

Example endpoint:

```http
GET /api/index.php/contacts?limit=100&page=0
```

Optional filtered endpoint:

```http
GET /api/index.php/contacts?thirdparty_ids=1,2,3&limit=100&page=0
```

---

## Database Changes

Dont Create table we Update code bellow business_contacts:

```php
Schema::create('business_contacts', function (Blueprint $table) {
    $table->id();

    $table->foreignId('company_id')->nullable()->constrained()->nullOnDelete();
    $table->foreignId('project_id')->nullable()->constrained()->nullOnDelete();

    $table->unsignedBigInteger('dolibarr_thirdparty_id')->nullable()->index();
    $table->unsignedBigInteger('dolibarr_contact_id')->nullable()->index();

    $table->string('source', 30)->default('manual'); 
    // manual | dolibarr

    $table->string('type', 50)->default('client'); 
    // client | prospect | supplier | contact

    $table->string('civility', 30)->nullable();
    $table->string('name');
    $table->string('lastname')->nullable();
    $table->string('poste')->nullable();

    $table->string('address')->nullable();
    $table->string('tel')->nullable();
    $table->string('email')->nullable();

    $table->decimal('lat', 10, 7)->nullable();
    $table->decimal('lng', 10, 7)->nullable();
	we already have 	map_link
    $table->timestamp('synced_at')->nullable();

    $table->timestamps();

    $table->unique(
        ['source', 'dolibarr_thirdparty_id', 'dolibarr_contact_id'],
        'business_contacts_dolibarr_unique'
    );
});
```

---

## Model

Update :

```text
app/Models/BusinessContact.php
```

Expected fillable:

```php
protected $fillable = [
    'company_id',
    'project_id',
    'dolibarr_thirdparty_id',
    'dolibarr_contact_id',
    'source',
    'type',
    'civility',
    'name',
    'lastname',
    'poste',
    'address',
    'tel',
    'email',
    'lat',
    'lng',
    'synced_at',
	'map_link'
];
```

Expected casts:

```php
protected $casts = [
    'lat' => 'decimal:7',
    'lng' => 'decimal:7',
    'synced_at' => 'datetime',
];
```

Useful accessors:

```php
public function getFullNameAttribute(): string
{
    return trim(($this->name ?? '') . ' ' . ($this->lastname ?? ''));
}
```

---

## Dolibarr Configuration

Add config values in `.env`:

```env
DOLIBARR_BASE_URL=https://your-dolibarr-domain.com
DOLIBARR_API_KEY=your_api_key
DOLIBARR_TIMEOUT=30
```

Create config file:

```text
config/dolibarr.php
```

```php
return [
    'base_url' => env('DOLIBARR_BASE_URL'),
    'api_key' => env('DOLIBARR_API_KEY'),
    'timeout' => env('DOLIBARR_TIMEOUT', 30),
];
```

---

## Service Layer

Create:

```text
app/Services/Dolibarr/DolibarrClient.php
app/Services/Dolibarr/DolibarrContactSyncService.php
```

### DolibarrClient

Responsibilities:

- Store base URL.
- Add `DOLAPIKEY` header.
- Add `Accept: application/json`.
- Handle timeout.
- Throw clean exceptions on API failure.

Example:

```php
namespace App\Services\Dolibarr;

use Illuminate\Support\Facades\Http;

class DolibarrClient
{
    public function get(string $endpoint, array $query = []): array
    {
        return Http::withHeaders([
                'DOLAPIKEY' => config('dolibarr.api_key'),
                'Accept' => 'application/json',
            ])
            ->timeout((int) config('dolibarr.timeout', 30))
            ->baseUrl(rtrim(config('dolibarr.base_url'), '/') . '/api/index.php')
            ->get($endpoint, $query)
            ->throw()
            ->json();
    }
}
```

---

## Sync Service

Create:

```text
app/Services/Dolibarr/DolibarrContactSyncService.php
```

Responsibilities:

1. Sync Dolibarr clients from Thirdparties API.
2. Sync Dolibarr physical contacts from Contacts API.
3. Use pagination.
4. Use `updateOrCreate`.
5. Store `synced_at`.
6. Return sync statistics.

### Sync Clients

```php
public function syncClients(): int
{
    $page = 0;
    $limit = 100;
    $count = 0;

    do {
        $items = $this->client->get('/thirdparties', [
            'mode' => 1,
            'limit' => $limit,
            'page' => $page,
        ]);

        foreach ($items as $tier) {
            BusinessContact::updateOrCreate(
                [
                    'source' => 'dolibarr',
                    'dolibarr_thirdparty_id' => $tier['id'] ?? $tier['rowid'],
                    'dolibarr_contact_id' => null,
                ],
                [
                    'type' => 'client',
                    'name' => $tier['name'] ?? $tier['nom'] ?? 'Sans nom',
                    'address' => $tier['address'] ?? null,
                    'tel' => $tier['phone'] ?? null,
                    'email' => $tier['email'] ?? null,
                    'lat' => $tier['lat'] ?? null,
                    'lng' => $tier['long'] ?? null,
                    'synced_at' => now(),
                ]
            );

            $count++;
        }

        $page++;
    } while (count($items) === $limit);

    return $count;
}
```

### Sync Contacts

```php
public function syncContacts(): int
{
    $page = 0;
    $limit = 100;
    $count = 0;

    do {
        $items = $this->client->get('/contacts', [
            'limit' => $limit,
            'page' => $page,
        ]);

        foreach ($items as $contact) {
            BusinessContact::updateOrCreate(
                [
                    'source' => 'dolibarr',
                    'dolibarr_contact_id' => $contact['id'] ?? $contact['rowid'],
                ],
                [
                    'dolibarr_thirdparty_id' => $contact['socid'] ?? $contact['fk_soc'] ?? null,
                    'type' => 'contact',
                    'name' => $contact['firstname'] ?? $contact['name'] ?? '',
                    'lastname' => $contact['lastname'] ?? null,
                    'civility' => $contact['civility_code'] ?? null,
                    'poste' => $contact['poste'] ?? null,
                    'address' => $contact['address'] ?? null,
                    'tel' => $contact['phone_mobile'] ?? $contact['phone_pro'] ?? null,
                    'email' => $contact['email'] ?? null,
                    'synced_at' => now(),
                ]
            );

            $count++;
        }

        $page++;
    } while (count($items) === $limit);

    return $count;
}
```

---

## Artisan Command

Create:

```text
app/Console/Commands/SyncDolibarrContactsCommand.php
```

Command name:

```bash
php artisan dolibarr:sync-contacts
```

Responsibilities:

- Sync clients.
- Sync contacts.
- Print total synced.
- Catch errors and print a readable message.

Example output:

```text
Syncing Dolibarr clients...
Clients synced: 350

Syncing Dolibarr contacts...
Contacts synced: 820

Done.
```

---

## Queue Job

Create optional queued job:

```text
app/Jobs/SyncDolibarrContactsJob.php
```

Responsibilities:

- Run the same service.
- Useful for admin button.
- Prevent request timeout.
- Store sync result in database or logs.

---

## Sync Logs

Create table:

```text
dolibarr_sync_logs
```

Suggested columns:

```php
Schema::create('dolibarr_sync_logs', function (Blueprint $table) {
    $table->id();

    $table->string('type'); 
    // contacts | clients | full

    $table->string('status')->default('running'); 
    // running | success | failed

    $table->unsignedInteger('clients_count')->default(0);
    $table->unsignedInteger('contacts_count')->default(0);

    $table->text('error_message')->nullable();

    $table->timestamp('started_at')->nullable();
    $table->timestamp('finished_at')->nullable();

    $table->foreignId('started_by')->nullable()->constrained('users')->nullOnDelete();

    $table->timestamps();
});
```

---

## Admin UI

Create page:

```text
Settings > Integrations > Dolibarr
```

Required UI blocks:

### 1. Configuration Card

Show:

- Dolibarr base URL.
- API key configured: yes/no.
- Test connection button.
- Last successful sync date.

### 2. Sync Actions

Buttons:

- Test connection.
- Sync clients.
- Sync contacts.
- Full sync.

### 3. Sync Status

Show:

- Running / success / failed.
- Number of clients synced.
- Number of contacts synced.
- Error message if failed.
- Started by.
- Started at.
- Finished at.

---

## Business Contacts UI

Create page:

```text
Business Contacts
```

Features:

- Search by name, lastname, email, tel, address.
- Filter by type:
  - client
  - prospect
  - supplier
  - contact
- Filter by source:
  - manual
  - dolibarr
- Show Dolibarr badge when source is Dolibarr.
- Show client company and linked physical contacts.
- Allow manual contacts.
- Do not edit Dolibarr contacts directly in first version.

---

## Affaire Integration

In Affaire create/edit form:

Add field:

```text
Business Contact
```

Behavior:

- Search/select from `business_contacts`.
- Display:
  - name / lastname
  - type
  - tel
  - email
  - address
- Save selected `business_contact_id` on affaire.

Migration example:

```php
Schema::table('affaires', function (Blueprint $table) {
    $table->foreignId('business_contact_id')
        ->nullable()
        ->after('project_id')
        ->constrained('business_contacts')
        ->nullOnDelete();
});
```

---

## Project Integration

In Project create/edit form:

Add optional field:

```text
Client / Business Contact
```

Migration example:

```php
Schema::table('projects', function (Blueprint $table) {
    $table->foreignId('business_contact_id')
        ->nullable()
        ->after('company_id')
        ->constrained('business_contacts')
        ->nullOnDelete();
});
```

---

## API Endpoints in Trakyo

Suggested routes:

```php
Route::middleware(['auth'])->group(function () {
    Route::get('/business-contacts', [BusinessContactController::class, 'index'])
        ->name('business-contacts.index');

    Route::get('/business-contacts/search', [BusinessContactController::class, 'search'])
        ->name('business-contacts.search');

    Route::post('/business-contacts', [BusinessContactController::class, 'store'])
        ->name('business-contacts.store');

    Route::put('/business-contacts/{businessContact}', [BusinessContactController::class, 'update'])
        ->name('business-contacts.update');

    Route::delete('/business-contacts/{businessContact}', [BusinessContactController::class, 'destroy'])
        ->name('business-contacts.destroy');

    Route::post('/integrations/dolibarr/test', [DolibarrIntegrationController::class, 'test'])
        ->name('integrations.dolibarr.test');

    Route::post('/integrations/dolibarr/sync', [DolibarrIntegrationController::class, 'sync'])
        ->name('integrations.dolibarr.sync');

    Route::get('/integrations/dolibarr/sync-status', [DolibarrIntegrationController::class, 'syncStatus'])
        ->name('integrations.dolibarr.sync-status');
});
```

---

## BusinessContactController

Needed actions:

### index

Return Inertia page with paginated contacts.

Filters:

- search
- type
- source

### search

Return JSON for autocomplete.

Example response:

```json
[
  {
    "id": 1,
    "label": "Société ABC",
    "type": "client",
    "tel": "12345678",
    "email": "contact@abc.com",
    "source": "dolibarr"
  }
]
```

### store

Allow manual contact creation.

Set:

```php
'source' => 'manual'
```

### update

Allow update only if:

```php
$businessContact->source === 'manual'
```

For Dolibarr contacts, either block editing or allow local override later.

### destroy

Recommended first version:

- Allow deleting manual contacts.
- Do not delete Dolibarr contacts.
- For Dolibarr records, use hide/archive later if needed.

---

## Validation Rules

For manual contacts:

```php
[
    'type' => ['required', 'string', 'max:50'],
    'name' => ['required', 'string', 'max:255'],
    'lastname' => ['nullable', 'string', 'max:255'],
    'civility' => ['nullable', 'string', 'max:30'],
    'poste' => ['nullable', 'string', 'max:255'],
    'address' => ['nullable', 'string'],
    'tel' => ['nullable', 'string', 'max:50'],
    'email' => ['nullable', 'email', 'max:255'],
    'lat' => ['nullable', 'numeric'],
    'lng' => ['nullable', 'numeric'],
]
```

---

## Permissions

Add permissions:

```text
view_business_contacts
create_business_contacts
edit_business_contacts
delete_business_contacts

view_integrations
manage_dolibarr_integration
sync_dolibarr_contacts
```

Recommended behavior:

- Normal users can view/search contacts.
- Admin can manage sync.
- Only admin can configure Dolibarr.
- Manual contact edit depends on permission.

---

## Frontend Components

Suggested React/Inertia files:

```text
resources/js/pages/business-contacts/index.tsx
resources/js/pages/settings/integrations/dolibarr.tsx
resources/js/components/business-contacts/business-contact-select.tsx
resources/js/components/business-contacts/business-contact-form.tsx
resources/js/components/business-contacts/business-contact-card.tsx
```

### BusinessContactSelect

Reusable component for Affaire and Project forms.

Props:

```ts
type BusinessContactSelectProps = {
    value?: number | null;
    onChange: (id: number | null) => void;
    placeholder?: string;
};
```

Behavior:

- Search API with debounce.
- Show name, tel, email, type, source.
- Badge for Dolibarr/manual.
- Clear selected contact.

---

## Testing Plan

### Unit Tests

Test:

- Dolibarr API client builds correct headers.
- Client sync maps thirdparty data correctly.
- Contact sync maps contact data correctly.
- Existing records are updated, not duplicated.
- Missing optional fields do not crash sync.

### Feature Tests

Test:

- Admin can trigger sync.
- Non-admin cannot trigger sync.
- Business contacts page loads.
- Search returns correct contacts.
- Manual contact can be created.
- Dolibarr contact cannot be edited in first version.
- Affaire can save `business_contact_id`.
- Project can save `business_contact_id`.

### Edge Cases

Test:

- Dolibarr API unavailable.
- Wrong API key.
- Empty response.
- Contact without email.
- Contact without thirdparty.
- Duplicate Dolibarr IDs.
- Very large contact list with pagination.

---

## Implementation Steps

### Step 1 - Database

- Create `business_contacts` migration.
- Create `dolibarr_sync_logs` migration.
- Add `business_contact_id` to `affaires`.
- Add `business_contact_id` to `projects`.

### Step 2 - Models

- Create `BusinessContact` model.
- Create `DolibarrSyncLog` model.
- Add relations in `Affaire`.
- Add relations in `Project`.

### Step 3 - Config

- Add `.env` keys.
- Create `config/dolibarr.php`.

### Step 4 - Dolibarr Services

- Create `DolibarrClient`.
- Create `DolibarrContactSyncService`.
- Implement clients sync.
- Implement contacts sync.
- Add error handling.

### Step 5 - Command and Job

- Create artisan command `dolibarr:sync-contacts`.
- Create queued job `SyncDolibarrContactsJob`.

### Step 6 - Admin Integration UI

- Create Dolibarr settings/integration page.
- Add test connection.
- Add sync button.
- Add sync status display.

### Step 7 - Business Contacts UI

- Create index page.
- Create search/filter.
- Create manual create/edit form.
- Add badges for source/type.

### Step 8 - Affaire and Project Integration

- Add `BusinessContactSelect` component.
- Add field to Affaire form.
- Add field to Project form.
- Persist `business_contact_id`.

### Step 9 - Permissions

- Add permissions.
- Gate admin sync routes.
- Gate create/edit/delete actions.

### Step 10 - Tests

- Add unit tests.
- Add feature tests.
- Test Dolibarr API failure scenarios.
- Test pagination.

---

## First Version Rules

For version 1:

- Sync from Dolibarr to Trakyo only.
- Do not push data from Trakyo to Dolibarr.
- Do not edit Dolibarr contacts directly.
- Manual contacts are editable.
- Dolibarr contacts are refreshed by sync.
- Keep Dolibarr IDs in database.

---

## Future Improvements

Later versions can add:

- Two-way sync.
- Conflict resolution.
- Local overrides for Dolibarr records.
- Geocoding address to lat/lng.
- Map view for business contacts.
- Contact categories.
- Contact roles by project or affaire.
- Link multiple contacts to one affaire.
- Webhook or scheduled sync.
- Contact import from Excel.
