← Tous les designs

gauth-0004-create-more-audiences-and-categories · frontend/src

Spec d'implémentation — Nudges Audiences & Catégories

Spec de développement pour le design Nudges - Contexte Réel du Code (Ant Design).dc.html. Repo : RandomCoffee/randomcoffee · branche master · frontend frontend/src.

Deux livrables distincts :

Principe directeur : aucun nouveau concept, aucun nouvel écran. Neuf nudges sur onze réutilisent une chaîne gettext() déjà traduite et un composant déjà monté sur la page. Deux seulement demandent du backend.


0. Composants introduits

Trois composants nouveaux au total. Tout le reste est de la composition avec l'existant.

0.1 <AudienceSuggestionCards /> — nouveau

frontend/src/components/audience-suggestions/index.tsx

Grille de cartes d'audiences proposées, cochables. Utilisé par 1a, 1h, 2d.

type AudienceSuggestion = {
  key: string;              // stable, pour la sélection
  name: string;             // "New joiners"
  type: AUDIENCES_TYPES;    // DYNAMIC par défaut
  filters: Filters;         // format filters existant, cf. utils/filters
  description: string;      // règle en langage naturel : "Office is Paris"
  count: number | null;     // null = en cours de calcul
  source?: 'csv' | 'hris' | 'categories';
};

type Props = {
  suggestions: AudienceSuggestion[];
  value: string[];                        // keys cochées
  onChange: (keys: string[]) => void;
  loading?: boolean;
  columns?: number;                       // défaut 3
};

Composition Ant : Row gutter={[8,8]} + Col + Card size="small" + Checkbox + Tag icon={<SyncOutlined />} pour le type + Text type="secondary" pour le compte.

Comptes : un seul appel groupé sur get_filters_count par suggestion (même service que CategoriesTreeSelect), avec Skeleton.Button tant que count === null. Ne pas bloquer le rendu des cartes sur les comptes.

0.2 <AudienceEmptyHint /> — nouveau

frontend/src/components/audience-empty-hint/index.tsx

Le bloc Empty + CTA réutilisé dans tous les états vides (1c, 1d, 1j). Existe déjà en substance dans components/audience-select/index.tsx (notFoundContent) — extraire ce JSX plutôt que le réécrire.

type Props = {
  kind: 'categories' | 'audiences';
  layout?: 'dropdown' | 'inline';   // dropdown = centré, inline = horizontal
  onCreate?: () => void;            // défaut : history.push vers la route de création
};

Copy : réutiliser mot pour mot les chaînes existantes, aucune nouvelle traduction.

kindtitredescriptionbouton
categoriesNo categories for nowCategories let you categorize your coworkers by things like departments and location.Create New Category
audiencesNo audience foundYou've got no audiences for the momentCreate a new Audience

Les trois premières viennent de pages/account/admin/categories/empty.tsx, les autres de audience-select/index.tsx et admin/audiences/index.tsx.

Gate permissions : envelopper le bouton dans <PermissionAction hasPermission={permissions.hasCreateAudiencesPermission()}> pour le kind audiences — même traitement que dans audience-select.

0.3 hasAudiences(state) — nouveau sélecteur

frontend/src/utils/onboarding.ts

export function hasAudiences(state) {
  return state.audiences.pagination.count > 0;
}

Utilisé par 1g (milestone) et 1i (condition d'affichage de l'Alert). Un seul point de vérité, à ne pas dupliquer.


Partie A — Flow Import CSV → audience dynamique

Fichiers touchés :

pages/account/admin/users/add/csv/index.tsx        écrans 2a, 2c, 2d
pages/account/admin/users/add/csv/mapping_item.tsx écran 2b
pages/account/admin/users/list/audience-filters.tsx écran 2e
utils/upload.ts                                    dérivation des suggestions

A.1 Écran 2a — Upload

Aucun changement. Rappel utile pour la suite : Papa.parse lit le fichier côté client dès le dépôt (beforeUpload retourne false). Les colonnes et leurs valeurs sont donc connues avant toute requête serveur — c'est ce qui rend les suggestions des écrans suivants calculables sans backend.

A.2 Écran 2b — Map Columns

Fichier : mapping_item.tsx, dans NewFieldsOptions.

Ajouter sous les champs existants, uniquement quand mapping.status === 'NEW' et mapping.type est dans [select, boolean, date] :

<Alert
  type="info"
  showIcon
  message={interpolate(
    gettext('%(count)s values detected. This category can become a matching rule.'),
    { count: uniqueValues.length },
  )}
  description={
    <Checkbox
      checked={mapping.suggestAudiences !== false}
      onChange={(e) => updateMapping({ ...mapping, suggestAudiences: e.target.checked })}
    >
      {gettext('Suggest audiences from this column after the import')}
    </Checkbox>
  }
/>

A.3 Écran 2c — Traitement

Aucun changement.

A.4 Écran 2d — Result de succès

Fichier : csv/index.tsx, composant UploadStatus.

Aujourd'hui extra ne contient que « Go to my upload history » et « Resubmit a new list ». Nouvelle structure :

<Result
  status="success"
  title={/* inchangé */}
  subTitle={/* inchangé */}
  extra={[
    <AudienceSuggestionBlock
      key="audiences"
      categories={createdCategories}
      suggestions={suggestions}
    />,
    ...existingButtons,   // conservés, en secondaire
  ]}
/>

AudienceSuggestionBlock = Alert type="info" (le titre et la promesse) + <AudienceSuggestionCards /> + un Button type="primary" dont le libellé porte le compte :

interpolate(ngettext('Create %(count)s audience', 'Create %(count)s audiences', n), { count: n })

Dérivation des suggestions — à ajouter dans utils/upload.ts, fonction pure, testable sans réseau :

export function getAudienceSuggestions(csvData, mappings): AudienceSuggestion[]

Règles, dans cet ordre de priorité, plafonné à 3 suggestions :

Condition sur le mappingSuggestion produitefilters
type === 'date' et titre matche start/hire/joiningNew joinersdate, delta 90 jours
type === 'select' avec ≤ 8 valeurs distinctesune par valeur, la plus peuplée d'abordeq sur la valeur
type === 'select' avec > 8 valeursaucune (trop bruyant)
colonne manager_email / manager non videManagersprésence d'au moins un rattachement

Ne suggérer que les colonnes dont suggestAudiences !== false. Toutes les suggestions sont AUDIENCES_TYPES.DYNAMIC.

Séquencement à respecter : onUploadOver dispatch déjà categories/getCategories. Les suggestions doivent être calculées après ce dispatch, sinon les IDs de catégories fraîchement créées ne sont pas encore dans le store et les filters pointent dans le vide.

Tracking : invite:csv:audience:suggested { count } à l'affichage du bloc, users:audiences:created { type: 1, source: 'csv' } à la création — le second existe déjà dans audience-filters.tsx, ajouter simplement la clé source.

A.5 Écran 2e — Modal de confirmation

Réutiliser AudienceModal de pages/account/admin/users/list/audience-filters.tsx tel quel. Deux extensions :

  1. Accepter initialValues={{ name, type, filters }} pour arriver pré-rempli — le composant lit aujourd'hui les filtres depuis useSelector(({ users }) => users.filters), il faut permettre de les passer en prop, avec fallback sur le store.
  2. Ajouter une Checkbox sous les RadioCards : « Use it right away in a program ». Cochée, la redirection après création va vers /admin/campaigns/new avec l'audience pré-sélectionnée au lieu de /admin/users/audiences.

Un seul modal, même si plusieurs audiences sont cochées. Il porte alors le nom de la première et un Text type="secondary" « + 1 other audience will be created with the same settings ». Enchaîner N modals identiques est le principal risque d'abandon de ce flow.

A.6 Écran 2f — Atterrissage

Aucun changement de code. Le toast (Your audience has been created successfully) et la redirection existent déjà dans onSaveAudience. Seule différence observable : la liste n'est plus vide et le milestone 1g se coche.

A.7 Écran 2g — Payoff

Aucun changement de code. À vérifier en recette : l'audience créée apparaît bien dans AudiencesSelect du builder sans rechargement (le composant maintient un audiences_cache — vérifier que audiences/addToCache est bien alimenté après création).


Partie B — Les 11 nudges

1a — Result de fin d'import

Couvert par la Partie A (§A.4).

1b — Étape Map Columns

Couvert par la Partie A (§A.2).

1c — CategoriesTreeSelect sans état vide

components/categories-tree-select/index.tsx

Le TreeSelect n'a pas de notFoundContent. Ajouter :

notFoundContent={
  tree.length === 0
    ? <AudienceEmptyHint kind="categories" layout="dropdown" />
    : undefined
}

Le composant voisin AudiencesSelect a déjà exactement ce pattern — s'aligner dessus, ne pas inventer.

1d — Règles de matching sur un Select vide

pages/account/admin/campaigns/rules/by-category.tsx et rules/audiences.tsx

Le second RuleSelector reste volontairement vide tant qu'aucune catégorie n'est choisie — c'est correct, ne pas y toucher.

1e — PeopleCount : l'avertissement devient actionnable

components/categories-tree-select/index.tsx, export PeopleCount

Le composant accepte déjà une prop onClick, mais elle n'est pas exploitée dans la branche warning. Dans cette branche, rendre un Alert type="warning" au lieu du div nu, avec :

Un seul correctif ici couvre le ciblage de campagne, les broadcasts et les permissions : le composant est réutilisé par CategoriesTreeSelect et AudiencesSelect.

1f — Liste des coworkers, sélection multiple

pages/account/admin/users/list/selected_users_alert.tsx

La création d'audience est aujourd'hui enterrée dans ActionsDropdown. Ajouter un Typography.Link « Save as an audience » directement dans la barre, après « Select all coworkers », séparé par le Divider type="vertical" déjà en place. Il ouvre l'AudienceModal existant, avec le nom pré-rempli depuis le filtre actif si un seul filtre est posé.

Ne pas retirer l'entrée du dropdown : les deux chemins coexistent.

1g — Milestone d'onboarding

components/onboarding/admin-milestones.tsx + utils/onboarding.ts

  1. Ajouter hasAudiences (§0.3) et l'insérer dans adminCheckers — le compteur passe automatiquement à x / 4 puis x / 8 avec les checkers end-user, onboardingPercent n'a pas besoin d'être touché.
  2. Ajouter le milestone dans getMilestones(), en 3ᵉ position, avant « Launch your first campaign » : c'est l'ordre d'usage réel du produit, et ça rend le ciblage disponible au moment où l'admin ouvre le builder.
title:      Create your first audience
helpText:   Audiences let you reuse a group of coworkers — "New joiners", "Paris office",
            "Managers" — across every program and broadcast, instead of picking people one by one.
actionText: Create an audience
onAction:   history.push(`/account/${slug}/admin/users/audiences`)
key:        'audiences'
done:       hasAudiences
  1. Ajouter hasAudiences(state) à la condition admin de hasFinishedOnboarding.
  2. Tracking : onboarding:add:audiences:click, calqué sur onboarding:add:categories:click.

Bonus quasi gratuit : getOnboardingSteps() mappe déjà la route /admin/users/audiences vers ONBOARDING_MODAL_TYPES.AUDIENCES. Le canal des modal steps existe et n'a simplement pas de contenu côté données — un step d'explication peut être créé sans une ligne de front.

1h — Page Audiences vide

pages/account/admin/audiences/index.tsx

  1. Enrichir emptyOptions du NamespaceTable : garder la description existante et ajouter <AudienceSuggestionCards /> alimenté par les catégories du store (categories.data), avec la même dérivation qu'en §A.4 mais appliquée aux valeurs de catégories déjà en base.
  2. Bug de parcours à corriger : onAddAudienceClick fait history.push('/account/${slug}/users') — l'admin est éjecté vers une liste sans explication. Le remplacer par l'ouverture de l'AudienceModal, ou à défaut par /admin/users (liste admin) avec un message.info expliquant qu'il faut filtrer puis enregistrer.

1i — Alert sur la Use Case Library

pages/account/admin/campaigns/templates.tsx

Le seul nudge produit déjà en place est l'Alert type="info" showIcon action={<CreateCampaignButton />} (« Find your new team ritual with Programs »). C'est le format de référence : décliner le même composant, sous condition !hasAudiences(state) && hasCategories(state), avec icon={<TeamOutlined />} et un Button secondaire « Create audience ».

Une seule Alert visible à la fois : si celle des programmes s'affiche déjà, ne pas empiler.

1j — Deux boutons désactivés à expliquer

**pages/account/admin/users/list/categories-header.tsx** — le <Dropdown disabled={categories.length === 0}> rend le bouton œil inerte, sans explication. L'envelopper dans un Tooltip conditionnel :

Create a category to show extra columns here — for example Department or Office.

**pages/account/admin/settings/permissions/edit.tsx** — la section « Audiences » est souvent le premier endroit où un admin lit le mot « audience ». Sous le champ « Authorized audiences: », si zéro audience existe, afficher un Alert type="info" avec une définition en une phrase et un lien de création. Le Select reste en place, vide.

1k — Bannière après sync HRIS / SCIM

pages/account/admin/settings/hrsi.tsx (front) + chemin mass_upload / create_categories (back)

C'est le nudge le plus coûteux, à traiter en dernier. Le back crée déjà les catégories en silence pour toutes les sources (CSV, HRIS, SCIM) au même endroit — un seul point d'accroche couvre les trois.

L'écran HRIS promet déjà d'ajouter les nouveaux qualifiés aux campagnes « selon des critères d'audience ». La promesse est faite, l'outil pour la tenir n'est pas proposé.


Ordre de livraison suggéré

LotContenuCoûtDépendances
1AudienceEmptyHint + branchements 1c, 1d, 1jfaible — copy existanteaucune
21f lien dans la barre de sélection, 1e alerte actionnablefaibleaucune
3hasAudiences + 1g milestone + 1i Alertfaible, front seul§0.3
4AudienceSuggestionCards + Partie A complète + 1hmoyen§0.1, getAudienceSuggestions
51k bannière de syncfort — backend requisdiff de valeurs côté API

Points de vigilance transverses