# Documentation globale du dashboard Saham

Ce document explique le role de chaque dossier/fichier, puis le fonctionnement des parties importantes: `Parcours Acquisition` et `Campagnes Payantes` (`Paid Media`). Il montre aussi comment appeler les API, quel payload envoyer, comment lire les reponses et comment afficher les donnees dans le HTML Django.

## Vue generale

Le projet est une application Django.

Flux principal:

1. Le navigateur appelle la page `/`.
2. Django execute `dashboard/views.py:index`.
3. La vue construit les contextes `acquisition`, `paid_media` et `filters`.
4. Django rend `dashboard/templates/dashboard/index.html`.
5. `index.html` inclut les composants HTML: OKR, acquisition, paid media, EER.
6. `dashboard/static/dashboard/js/dashboard.js` active les tabs, les filtres multi-select, le graphique Chart.js et le fond Three.js.

Important: aujourd'hui, les donnees principales `acquisition` et `paid_media` sont lues depuis les fichiers Markdown:

- `parcours_acquisition.md`
- `Readmedataforgoogleads.md`
- `Readmedataformeta.md`

Les services API existent dans `dashboard/services/`, mais les appels dynamiques aux endpoints de performance ne sont pas encore branches dans `index()`. Le seul appel API dynamique utilise par le front est `/campaign-options/`, pour recuperer la liste des campagnes selon les canaux selectionnes.

## Dossiers et fichiers

### Racine du projet

#### `manage.py`

Commande principale Django. Sert a lancer le serveur, les migrations, le shell, etc.

Exemples:

```bash
python manage.py runserver
python manage.py migrate
```

#### `db.sqlite3`

Base SQLite locale configuree dans `config/settings.py`. Actuellement le dashboard ne semble pas utiliser de modele metier.

#### `README.md`

Ancienne documentation courte de l'arborescence.

#### `alldoc.md`

Ce fichier. Documentation complete du projet.

#### `parcours_acquisition.md`

Contient un exemple de payload et de reponse pour l'API GA4 acquisition funnel:

```text
/api/google-analytics/acquisitionFunnelGA4
```

La vue Django lit ce fichier avec `_load_parcours_acquisition_data()` puis extrait le premier objet JSON apres le mot `reponse`.

#### `Readmedataforgoogleads.md`

Contient un exemple de payload et de reponse pour Google Ads performance:

```text
/api/google-ads/getGoogleAdsCampaignsPerformance
```

La vue lit ce fichier dans `_load_paid_media_sources()` pour remplir la partie `google_ads` du paid media.

#### `Readmedataformeta.md`

Contient un exemple de payload et de reponse pour Meta Ads performance:

```text
/api/getMetaAdsCampaignsPerformancefunell
```

La vue lit ce fichier dans `_load_paid_media_sources()` pour remplir la partie `meta` du paid media.

#### `Selectcampaigns.md`

Documentation des endpoints qui retournent les IDs et noms des campagnes Google Ads et Meta Ads.

Ces endpoints sont utilises par `dashboard/services/campaign_options_service.py`.

#### `.gitignore`

Liste des fichiers/dossiers ignores par Git.

#### `venv/`

Environnement virtuel Python local. Ne contient pas de logique applicative.

### `config/`

Dossier de configuration Django.

#### `config/settings.py`

Configuration principale Django:

- `INSTALLED_APPS`: active `dashboard`.
- `DATABASES`: configure SQLite.
- `STATIC_URL`: configure les fichiers statiques.
- `DASHBOARD_BEARER_TOKEN`: token utilise par `api_client.py` si aucun token n'est envoye depuis le front.

Attention: un token ne doit pas rester en dur dans le code en production. Il vaut mieux le mettre dans une variable d'environnement.

#### `config/urls.py`

Routeur racine Django. Il doit inclure les URLs de l'app `dashboard`.

#### `config/asgi.py`

Point d'entree ASGI pour deployer Django avec un serveur compatible ASGI.

#### `config/wsgi.py`

Point d'entree WSGI pour deployer Django avec un serveur compatible WSGI.

#### `config/__init__.py`

Marque `config` comme package Python.

### `dashboard/`

Application Django principale.

#### `dashboard/urls.py`

Declare les routes de l'app:

```python
urlpatterns = [
    path('', index, name='dashboard'),
    path('campaign-options/', campaign_options, name='campaign_options'),
]
```

Routes disponibles:

- `/`: page principale du dashboard.
- `/campaign-options/`: endpoint JSON appele par le JS pour charger les campagnes.

#### `dashboard/views.py`

Fichier le plus important cote backend. Il contient:

- des helpers de formatage: `_format_number`, `_format_percent`, `_format_money`
- le parsing JSON depuis les fichiers Markdown: `_load_markdown_json`
- la construction du contexte acquisition: `_build_acquisition_context`
- la construction du contexte paid media: `_build_paid_media_context`
- la vue HTML principale: `index`
- l'endpoint JSON des campagnes: `campaign_options`

#### `dashboard/models.py`

Fichier vide actuellement. Il servirait a definir les modeles de base de donnees Django si necessaire.

### `dashboard/services/`

Contient les wrappers d'appels API.

#### `dashboard/services/api_client.py`

Client HTTP generique base sur `urllib`.

Fonctions principales:

- `get(path, params=None, bearer_token=None)`
- `post(path, payload=None, bearer_token=None)`

Il ajoute automatiquement:

```http
Content-Type: application/json
Authorization: Bearer <token>
```

Le token vient de:

1. l'argument `bearer_token`
2. `settings.DASHBOARD_BEARER_TOKEN`
3. variable d'environnement `DASHBOARD_BEARER_TOKEN`
4. variable d'environnement `VITE_BEARER_TOKEN`

Pour les chemins relatifs, il utilise `DASHBOARD_API_BASE_URL` ou `VITE_API_BASE_URL`.

Pour les URLs completes, par exemple `https://atworkflow.atirao.biz/...`, il appelle directement cette URL.

#### `dashboard/services/campaign_options_service.py`

Recupere les campagnes disponibles pour les filtres.

Google Ads:

```python
post(
    "https://atworkflow.atirao.biz/api/google-ads-data/getCampaignsinfo",
    {"client_id": client_id},
)
```

Meta:

```python
get(
    "https://atworkflow.atirao.biz/api/meta-ads-data/getCampaignsinfo",
    params={"client": client_id},
)
```

Puis `_map_campaigns()` transforme les reponses en options utilisables par le `<select>`:

```json
{
  "channel": "google_ads",
  "value": "google_ads:21232870217",
  "label": "Google Ads - SEA - Trafic -Campagne Depliant - Display Ads",
  "id": "21232870217",
  "name": "SEA - Trafic -Campagne Depliant - Display Ads"
}
```

#### `dashboard/services/google_ads_service.py`

Wrapper pour Google Ads:

```python
def get_google_ads_performance(payload):
    return post("/api/google-ads/getGoogleAdsCampaignsPerformance", payload)
```

Et recuperation des noms de campagnes:

```python
def get_campaign_names_google_ads(client_id):
    return post("/api/google-ads/getCampaignIdsAndNames", {"client_id": client_id})
```

#### `dashboard/services/meta_ads_service.py`

Wrapper pour Meta Ads:

```python
def get_meta_ads_performance(payload):
    return post("/api/getMetaAdsCampaignsPerformancefunell", payload)
```

Et recuperation des campagnes Meta:

```python
def get_campaign_names_meta_ads(client_id):
    return get("/api/metadashboard/getAdAccountsOrCampaignsOrAdSets", params={"client_id": client_id})
```

#### `dashboard/services/ga4_service.py`

Wrapper pour GA4 acquisition:

```python
def get_ga4_acquisition_funnel(payload):
    return post("/api/google-analytics/acquisitionFunnelGA4", payload)
```

#### `dashboard/services/campaigns.py`

Helpers pour normaliser des listes de campagnes venant d'API differentes.

Fonctions:

- `get_campaigns_array(data)`: trouve une liste dans `data`, `campaigns` ou `results`.
- `map_campaign(campaign)`: retourne `{id, label}`.
- `map_campaigns(data)`: applique `map_campaign` sur toute la liste.

### `dashboard/utils/`

#### `dashboard/utils/mappers.py`

Fichier vide actuellement.

#### `dashboard/utils/formatters.py`

Fichier vide actuellement.

Ces fichiers peuvent servir plus tard a deplacer les helpers de `views.py`.

### `dashboard/templates/dashboard/`

Templates HTML Django.

#### `base.html`

Layout global:

- charge Tailwind via CDN
- charge Phosphor Icons
- charge Three.js
- charge Chart.js
- charge `dashboard.css`
- inclut `header.html`, `tabs.html`, `filters.html`
- definit `<main>` ou `index.html` injecte les vues
- charge `dashboard.js`

#### `index.html`

Page principale. Elle inclut les composants:

```django
{% include 'dashboard/components/executive_view.html' %}
{% include 'dashboard/components/acquisition_view.html' %}
{% include 'dashboard/components/paid_media_view.html' %}
{% include 'dashboard/components/eer_view.html' %}
```

#### `components/header.html`

Header avec logo Saham, titre et bouton dark/light mode.

#### `components/tabs.html`

Navigation par onglets:

- OKR: `data-tab="executive"`
- Parcours Acquisition: `data-tab="dh"`
- Campagnes Payantes: `data-tab="paid"`
- Parcours EER: `data-tab="app"`

Le JS active les sections avec `id="view-..."`.

#### `components/filters.html`

Formulaire de filtres:

- `client_id`
- dates current: `start_date`, `end_date`
- dates previous: `previous_since`, `previous_until`
- canaux: `channels`
- campagnes: `campaign_ids`

Le formulaire est en `method="get"`, donc les filtres passent dans l'URL:

```text
/?client_id=10&start_date=2026-03-01&end_date=2026-04-03&channels=google_ads&campaign_ids=google_ads:21232870217&tab=paid
```

#### `components/acquisition_view.html`

Affiche la vue `Parcours Acquisition`.

Donnees utilisees:

- `acquisition.stages`
- `acquisition.rates`
- `acquisition.paid_vs_organic`
- `acquisition.cpa_warning`

Il inclut aussi:

```django
{% include 'dashboard/components/funnel.html' %}
```

#### `components/funnel.html`

Affiche le funnel acquisition GA4.

Pour chaque etape:

```django
{% for stage in acquisition.stages %}
    <div class="funnel-stage {{ stage.height_class }}" title="{{ stage.source }}">
        <span>{{ stage.label }}</span>
        <strong>{{ stage.value }}</strong>
        <em class="{% if stage.variation_value < 0 %}text-red-500{% else %}text-emerald-500{% endif %}">
            {{ stage.variation }}
        </em>
    </div>
{% endfor %}
```

Puis les taux de conversion:

```django
{% for rate in acquisition.rates %}
    <div class="funnel-rate" title="{{ rate.label }}">
        {{ rate.value }}
        <small>{{ rate.label }}</small>
    </div>
{% endfor %}
```

#### `components/paid_media_view.html`

Affiche la vue `Campagnes Payantes`.

Donnees utilisees:

- `paid_media.stages`
- `paid_media.chart_labels`
- `paid_media.chart_values`
- `paid_media.campaigns`
- `paid_media.totals`

Le graphique Chart.js recupere ses donnees via:

```django
{{ paid_media.chart_labels|json_script:"paid-chart-labels" }}
{{ paid_media.chart_values|json_script:"paid-chart-values" }}
```

Puis `dashboard.js` lit ces scripts JSON.

#### `components/executive_view.html`

Vue OKR. Actuellement plusieurs donnees sont statiques dans le HTML.

#### `components/eer_view.html`

Vue Parcours EER/KYC. Actuellement plusieurs donnees sont statiques dans le HTML.

#### `components/chart_card.html`

Petit composant placeholder pour une carte graphique.

#### `components/kpi_cards.html`

Petit composant placeholder pour des KPIs.

### `dashboard/static/dashboard/`

Fichiers statiques.

#### `css/dashboard.css`

Styles custom du dashboard:

- variables light/dark
- cards glass
- inputs et multi-select
- tabs
- sections
- funnel
- progress rows

#### `js/dashboard.js`

JavaScript principal:

- `initThemeToggle()`: active/désactive `html.dark`.
- `initTabs()`: change la section visible selon les tabs.
- `initWebGLBackground()`: fond Three.js.
- `initPaidChart()`: cree le bar chart Chart.js des top campagnes.
- `initNativeMultiSelect()`: transforme un `<select multiple>` en dropdown avec checkbox.
- `initCampaignFilters()`: appelle `/campaign-options/` pour charger les campagnes selon les canaux.

## Parcours Acquisition

### Endpoint API

Service Python:

```python
from dashboard.services.ga4_service import get_ga4_acquisition_funnel
```

Endpoint:

```text
POST /api/google-analytics/acquisitionFunnelGA4
```

Exemple avec base URL:

```text
http://localhost:3000/api/google-analytics/acquisitionFunnelGA4
```

### Payload

```json
{
  "client_id": 31,
  "start_date": "2026-01-01",
  "end_date": "2026-01-20"
}
```

Champs:

- `client_id`: ID du client.
- `start_date`: date debut de la periode courante.
- `end_date`: date fin de la periode courante.

### Appel API cote Python

```python
from dashboard.services.ga4_service import get_ga4_acquisition_funnel

payload = {
    "client_id": 31,
    "start_date": "2026-01-01",
    "end_date": "2026-01-20",
}

data = get_ga4_acquisition_funnel(payload)
```

### Structure de reponse attendue

La vue utilise surtout:

```json
{
  "funnel": {
    "sessions_web": {
      "value": 46460,
      "previous": 52451,
      "variation_percent": -11.42,
      "source": "GA4 metric: sessions"
    },
    "downloads": {
      "value": 351,
      "previous": 375,
      "variation_percent": -6.4,
      "source": "GA4 eventCount where eventName = file_download/download/app_download"
    },
    "installs": {
      "value": 0,
      "previous": 0,
      "variation_percent": 0,
      "source": "GA4 eventCount where eventName = first_open/app_install/install"
    },
    "prospects": {
      "value": 0,
      "previous": 0,
      "variation_percent": 0,
      "source": "GA4 eventCount where eventName = generate_lead/lead_submit/prospect_created"
    },
    "new_accounts": {
      "value": 0,
      "previous": 0,
      "variation_percent": 0,
      "source": "GA4 eventCount where eventName = sign_up/account_created/new_account"
    }
  },
  "conversion_rates": {
    "session_to_download": 0.76,
    "download_to_install": 0,
    "install_to_prospect": 0,
    "prospect_to_account": 0
  },
  "paid_vs_organic": {
    "paid": 0,
    "organic": 0,
    "other": 0,
    "paid_percent": 0,
    "organic_percent": 0,
    "other_percent": 0
  },
  "warning": {
    "cpa": "CPA non inclus avec GA4 seule."
  }
}
```

### Transformation dans `views.py`

La fonction `_build_acquisition_context(data)` transforme la reponse API brute en contexte facile a afficher.

Exemple:

```python
stage_config = [
    ("sessions_web", "Sessions web", "h-32"),
    ("downloads", "Telechargements", "h-28"),
    ("installs", "Installations", "h-24"),
    ("prospects", "Prospects", "h-20"),
    ("new_accounts", "Nouveaux comptes", "h-16"),
]
```

Pour chaque key, elle construit:

```json
{
  "key": "sessions_web",
  "label": "Sessions web",
  "height_class": "h-32",
  "value": "46 460",
  "previous": "52 451",
  "variation": "-11.42%",
  "variation_value": -11.42,
  "source": "GA4 metric: sessions"
}
```

### Affichage HTML

Dans `acquisition_view.html`:

```django
{% include 'dashboard/components/funnel.html' %}
```

Dans `funnel.html`, chaque etape est affichee:

```django
<strong>{{ stage.value }}</strong>
<em class="{% if stage.variation_value < 0 %}text-red-500{% else %}text-emerald-500{% endif %}">
    {{ stage.variation }}
</em>
```

Donc:

- si variation negative: rouge
- sinon: vert

Le bloc paid vs organic utilise:

```django
style="width: {{ acquisition.paid_vs_organic.organic_percent }}%"
style="width: {{ acquisition.paid_vs_organic.paid_percent }}%"
```

## Paid Media / Campagnes Payantes

La vue Paid Media combine Google Ads et Meta Ads.

### Endpoint Google Ads performance

Service Python:

```python
from dashboard.services.google_ads_service import get_google_ads_performance
```

Endpoint:

```text
POST /api/google-ads/getGoogleAdsCampaignsPerformance
```

Payload:

```json
{
  "client_id": 8,
  "campaign_ids": [21232870217],
  "since": "2026-03-01",
  "until": "2026-04-03",
  "previous_since": "2026-01-26",
  "previous_until": "2026-02-28"
}
```

Notes:

- `campaign_ids` peut etre une liste vide pour toutes les campagnes.
- `since` / `until` = periode courante.
- `previous_since` / `previous_until` = periode de comparaison.

Appel Python:

```python
from dashboard.services.google_ads_service import get_google_ads_performance

payload = {
    "client_id": 8,
    "campaign_ids": [21232870217],
    "since": "2026-03-01",
    "until": "2026-04-03",
    "previous_since": "2026-01-26",
    "previous_until": "2026-02-28",
}

data = get_google_ads_performance(payload)
```

### Endpoint Meta Ads performance

Service Python:

```python
from dashboard.services.meta_ads_service import get_meta_ads_performance
```

Endpoint:

```text
POST /api/getMetaAdsCampaignsPerformancefunell
```

Payload:

```json
{
  "client_id": 5,
  "since": "2026-04-01",
  "until": "2026-04-30",
  "previous_since": "2026-03-02",
  "previous_until": "2026-03-31"
}
```

Appel Python:

```python
from dashboard.services.meta_ads_service import get_meta_ads_performance

payload = {
    "client_id": 5,
    "since": "2026-04-01",
    "until": "2026-04-30",
    "previous_since": "2026-03-02",
    "previous_until": "2026-03-31",
}

data = get_meta_ads_performance(payload)
```

### Structure de reponse attendue

La vue utilise ces blocs:

```json
{
  "totals": {
    "current": {
      "impressions": 5238062,
      "clicks": 366454,
      "cost": 21182.06,
      "downloads": 7154.25,
      "prospects": 7154.25,
      "accounts": 7154.25,
      "ctr": 7,
      "cpc": 0.06,
      "cpl": 2.96,
      "cpa": 2.96
    },
    "previous": {
      "impressions": 27015187,
      "clicks": 1958677,
      "cost": 132800.18,
      "downloads": 18916.84,
      "prospects": 18916.84,
      "accounts": 18916.84
    }
  },
  "campaigns": [
    {
      "campaign_id": 120206348834790282,
      "campaign_name": "Nom campagne",
      "current": {
        "impressions": 1489554,
        "clicks": 15246,
        "cost": 17291.04,
        "downloads": 0,
        "prospects": 294,
        "accounts": 0,
        "cpc": 1.13,
        "cpl": 58.81,
        "cpa": 0
      },
      "previous": {
        "impressions": 1000000,
        "clicks": 12000,
        "cost": 15000,
        "downloads": 0,
        "prospects": 250,
        "accounts": 0
      }
    }
  ],
  "top_5_campaigns_by_accounts": [
    {
      "campaign_id": 23051741001,
      "campaign_name": "SEA - TRAFFIC - PERFMAX Catalogue",
      "accounts": 5348.09,
      "cpc": 0.1,
      "cpl": 0.98,
      "cpa": 0.98
    }
  ]
}
```

Google Ads utilise souvent `top_5_campaigns_by_accounts`.

Meta utilise souvent `campaigns` avec `current` et `previous`.

La fonction `_campaign_source_rows(data)` normalise les deux formats.

### Transformation dans `views.py`

La fonction `_build_paid_media_context(sources, filters)` fait:

1. filtre les sources selon `filters["channels"]`
2. filtre les campagnes selon `filters["campaign_ids"]`
3. additionne `impressions`, `clicks`, `downloads`, `prospects`, `accounts`, `cost`
4. recalcule `ctr`, `cpc`, `cpl`, `cpa`
5. calcule les variations vs periode precedente
6. prepare:
   - `paid_media.stages`
   - `paid_media.totals`
   - `paid_media.campaigns`
   - `paid_media.chart_labels`
   - `paid_media.chart_values`

Exemple de contexte final:

```json
{
  "stages": [
    {
      "label": "Impressions",
      "height_class": "h-32",
      "value": "5 238 062",
      "variation": "-80.61%",
      "variation_value": -80.61
    }
  ],
  "totals": {
    "cost": "21 182.06 MAD",
    "ctr": "7.00%",
    "cpc": "0.06 MAD",
    "cpa": "2.96 MAD"
  },
  "campaigns": [
    {
      "platform": "Google Ads",
      "name": "SEA - TRAFFIC - PERFMAX Catalogue",
      "accounts": 5348.09,
      "accounts_label": "5 348.09",
      "cpc": "0.10 MAD",
      "cpl": "0.98 MAD",
      "cpa": "0.98 MAD"
    }
  ],
  "chart_labels": ["SEA - TRAFFIC - PERFMAX Catalogue"],
  "chart_values": [5348.09]
}
```

### Affichage HTML paid media

#### Funnel paid media

Dans `paid_media_view.html`:

```django
{% for stage in paid_media.stages %}
    <div class="funnel-stage {{ stage.height_class }}">
        <span>{{ stage.label }}</span>
        <strong>{{ stage.value }}</strong>
        <em class="{% if stage.variation_value < 0 %}text-red-500{% else %}text-emerald-500{% endif %}">
            {{ stage.variation }}
        </em>
    </div>
{% endfor %}
```

#### Chart top campagnes

Le template injecte les donnees JSON:

```django
{{ paid_media.chart_labels|json_script:"paid-chart-labels" }}
{{ paid_media.chart_values|json_script:"paid-chart-values" }}
```

Le JS les recupere:

```javascript
const labelsElement = document.getElementById("paid-chart-labels");
const valuesElement = document.getElementById("paid-chart-values");
const labels = labelsElement ? JSON.parse(labelsElement.textContent) : [];
const values = valuesElement ? JSON.parse(valuesElement.textContent) : [];
```

Puis Chart.js cree le graphique:

```javascript
new Chart(canvas, {
    type: "bar",
    data: {
        labels,
        datasets: [{
            label: "Comptes",
            data: values
        }]
    }
});
```

#### Tableau efficacite campagnes

Le template boucle sur `paid_media.campaigns`:

```django
{% for campaign in paid_media.campaigns %}
    <tr>
        <td>
            <span>{{ campaign.platform }}</span>
            {{ campaign.name }}
        </td>
        <td>{{ campaign.cpc }}</td>
        <td>{{ campaign.cpl }}</td>
        <td>{{ campaign.cpa }}</td>
    </tr>
{% empty %}
    <tr><td colspan="4">Aucune campagne pour ce filtre</td></tr>
{% endfor %}
```

#### KPI cout, CTR, CPC, CPA

```django
{{ paid_media.totals.cost }}
{{ paid_media.totals.ctr }}
{{ paid_media.totals.cpc }}
{{ paid_media.totals.cpa }}
```

## Filtres et chargement des campagnes

### Endpoint interne Django

Le front appelle:

```text
GET /campaign-options/?channels=google_ads&channels=meta&client_id=10
```

La route Django:

```python
path('campaign-options/', campaign_options, name='campaign_options')
```

La vue:

```python
def campaign_options(request):
    channels = _selected_values(request, "channels")
    client_id = request.GET.get("client_id", "10")
    authorization = request.headers.get("Authorization", "")
    bearer_token = authorization.removeprefix("Bearer ").strip() if authorization else None

    if not channels:
        channels = ["google_ads", "meta"]

    campaigns = get_campaign_options(channels, client_id, bearer_token=bearer_token)
    return JsonResponse({"campaigns": campaigns})
```

### Appel JS

Dans `dashboard.js`, `refreshCampaignOptions()` construit les query params:

```javascript
const params = new URLSearchParams();
selectedChannels.forEach((channel) => params.append("channels", channel));
params.set("client_id", clientIdInput.value);
```

Il recupere un token depuis le navigateur si disponible:

```javascript
const token =
    localStorage.getItem("token") ||
    localStorage.getItem("accessToken") ||
    localStorage.getItem("authToken");
const headers = token ? { Authorization: `Bearer ${token}` } : {};
```

Puis appelle Django:

```javascript
const response = await fetch(`/campaign-options/?${params.toString()}`, {
    headers,
});
const data = await response.json();
renderCampaignOptions(data.campaigns || []);
```

### Reponse attendue par le JS

```json
{
  "campaigns": [
    {
      "channel": "google_ads",
      "value": "google_ads:21232870217",
      "label": "Google Ads - SEA - Trafic -Campagne Depliant - Display Ads",
      "id": "21232870217",
      "name": "SEA - Trafic -Campagne Depliant - Display Ads"
    },
    {
      "channel": "meta",
      "value": "meta:120236803613890412",
      "label": "Meta - SMA_Meta_Trafic_Depliant",
      "id": "120236803613890412",
      "name": "SMA_Meta_Trafic_Depliant"
    }
  ]
}
```

### Affichage dans le select campaigns

`renderCampaignOptions(campaigns)` cree des `<option>`:

```javascript
const option = document.createElement("option");
option.value = campaign.value;
option.textContent = campaign.label;
option.dataset.channel = campaign.channel;
option.selected = selectedCampaigns.has(campaign.value);
campaignSelect.appendChild(option);
```

Ensuite `initNativeMultiSelect()` transforme ces options en checkbox visibles.

## Comment brancher les vraies API dans `index()`

Actuellement:

```python
acquisition_data = _load_parcours_acquisition_data()
paid_sources = _load_paid_media_sources()
```

Pour appeler les vraies API, il faut remplacer ce chargement Markdown par les services.

Exemple logique:

```python
from .services.ga4_service import get_ga4_acquisition_funnel
from .services.google_ads_service import get_google_ads_performance
from .services.meta_ads_service import get_meta_ads_performance

def index(request):
    filters = _build_filter_context(request, {})

    acquisition_payload = {
        "client_id": filters["client_id"],
        "start_date": filters["start_date"],
        "end_date": filters["end_date"],
    }
    acquisition_data = get_ga4_acquisition_funnel(acquisition_payload)

    paid_payload = {
        "client_id": filters["client_id"],
        "campaign_ids": [],
        "since": filters["start_date"],
        "until": filters["end_date"],
        "previous_since": filters["previous_since"],
        "previous_until": filters["previous_until"],
    }

    paid_sources = {
        "google_ads": {
            "label": "Google Ads",
            "data": get_google_ads_performance(paid_payload),
        },
        "meta": {
            "label": "Meta",
            "data": get_meta_ads_performance(paid_payload),
        },
    }

    return render(
        request,
        "dashboard/index.html",
        {
            "acquisition": _build_acquisition_context(acquisition_data),
            "filters": filters,
            "paid_media": _build_paid_media_context(paid_sources, filters),
        },
    )
```

Pour respecter les campagnes selectionnees, il faut convertir `filters["campaign_ids"]`.

Valeurs recues depuis le front:

```text
google_ads:21232870217
meta:120236803613890412
```

Conversion possible:

```python
google_campaign_ids = [
    value.split(":", 1)[1]
    for value in filters["campaign_ids"]
    if value.startswith("google_ads:")
]

meta_campaign_ids = [
    value.split(":", 1)[1]
    for value in filters["campaign_ids"]
    if value.startswith("meta:")
]
```

Puis envoyer chaque liste a son API.

## Points importants a retenir

- `views.py` prepare toutes les donnees affichees dans les templates.
- `acquisition_view.html` affiche le funnel GA4 et paid vs organic.
- `paid_media_view.html` affiche le funnel paid, le chart, le tableau campagnes et les KPIs.
- `dashboard.js` ne calcule pas les metriques paid media; il gere surtout l'interaction UI et le chargement des options de campagnes.
- Les endpoints performance existent dans les services, mais la page principale utilise encore des donnees Markdown.
- Pour passer en donnees live, il faut appeler `get_ga4_acquisition_funnel`, `get_google_ads_performance` et `get_meta_ads_performance` dans `index()`.
