Skip to main content

Vue d’ensemble

Les Workflows (Parcours Guidés) sont des tutoriels structurés qui accompagnent les associations dans leurs projets digitaux étape par étape. Objectif : Démystifier les outils numériques avec des guides concrets et actionnables. Statistiques :
  • 3 niveaux de difficulté (Débutant, Intermédiaire, Expert)
  • Durée estimée par workflow (30min à 2h)
  • Recommandations d’outils intégrées
  • Templates et checklists inclus

Architecture technique

Tables de base de données

Table workflows

CREATE TABLE workflows (
  id UUID PRIMARY KEY,
  title TEXT NOT NULL,
  description TEXT NOT NULL,
  difficulty TEXT CHECK (difficulty IN ('débutant', 'intermédiaire', 'expert')),
  duration TEXT NOT NULL,
  category TEXT NOT NULL,
  icon TEXT NOT NULL,
  status TEXT CHECK (status IN ('active', 'draft')),
  steps JSONB NOT NULL,
  display_order INTEGER DEFAULT 0,
  objective TEXT,
  completion_message TEXT,
  next_steps JSONB DEFAULT '[]',
  resources JSONB DEFAULT '[]',
  created_at TIMESTAMPTZ DEFAULT NOW(),
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

Table workflow_steps

CREATE TABLE workflow_steps (
  id UUID PRIMARY KEY,
  workflow_id UUID REFERENCES workflows(id) ON DELETE CASCADE,
  step_number INTEGER NOT NULL,
  tool_name TEXT NOT NULL,
  action TEXT NOT NULL,
  tool_url TEXT,
  detailed_instructions JSONB DEFAULT '[]',
  practical_tip TEXT,
  completion_checklist TEXT[],
  created_at TIMESTAMPTZ DEFAULT NOW()
);

API Layer (GraphQL)

Fichier : src/api/workflows.ts Toutes les opérations passent par apiCall() / apiCallVoid() + nhost.graphql.request().
import { apiCall, apiCallVoid } from './base';
import { nhost } from './client';

export const workflowsApi = {
  /**
   * Liste tous les workflows actifs
   */
  async list(): Promise<Workflow[]> {
    const query = `
      query GetWorkflows {
        workflows(
          where: { status: { _eq: "active" } }
          order_by: { display_order: asc }
        ) {
          id title description difficulty duration
          category icon status steps display_order
        }
      }
    `;
    return apiCall(
      () => nhost.graphql.request({ query }),
      'workflows.list'
    );
  },

  /**
   * Récupère un workflow par ID avec ses steps
   */
  async getById(id: string): Promise<Workflow> {
    const query = `
      query GetWorkflow($id: uuid!) {
        workflows_by_pk(id: $id) {
          id title description difficulty duration
          category icon status steps display_order
          objective completion_message
          workflow_steps(order_by: { step_number: asc }) {
            id step_number tool_name action tool_url
            detailed_instructions practical_tip completion_checklist
          }
        }
      }
    `;
    return apiCall(
      () => nhost.graphql.request({ query, variables: { id } }),
      'workflows.getById'
    );
  },

  /**
   * Met à jour un workflow (convention Hasura : _set)
   */
  async update(id: string, updates: Partial<Workflow>): Promise<Workflow> {
    const mutation = `
      mutation UpdateWorkflow($id: uuid!, $set: workflows_set_input!) {
        update_workflows_by_pk(pk_columns: { id: $id }, _set: $set) {
          id title status
        }
      }
    `;
    return apiCall(
      () => nhost.graphql.request({
        query: mutation,
        variables: { id, set: { ...updates, updated_at: new Date().toISOString() } }
      }),
      'workflows.update'
    );
  },

  /**
   * Supprime un workflow (cascade sur steps)
   */
  async delete(id: string): Promise<void> {
    const mutation = `
      mutation DeleteWorkflow($id: uuid!) {
        delete_workflows_by_pk(id: $id) { id }
      }
    `;
    await apiCallVoid(
      () => nhost.graphql.request({ query: mutation, variables: { id } }),
      'workflows.delete'
    );
  },
};

Structure d’un workflow

Métadonnées

interface Workflow {
  id: string;
  title: string;                    // "Créer ma première newsletter"
  description: string;               // Résumé du parcours
  difficulty: 'débutant' | 'intermédiaire' | 'expert';
  duration: string;                  // "30min", "1h", "2h"
  category: string;                  // "Communication", "Site Web", etc.
  icon: string;                      // Nom d'icône Lucide (ex: "Mail")
  status: 'active' | 'draft';
  display_order: number;
  objective?: string;
  completion_message?: string;
}

Steps (étapes)

interface WorkflowStep {
  step_number: number;
  tool_name: string;                 // "Mailchimp"
  action: string;                    // "Créer votre compte"
  tool_url?: string;
  detailed_instructions: Array<{
    text: string;
    type: 'text' | 'code' | 'link';
  }>;
  practical_tip?: string;
  completion_checklist?: string[];
}

Permissions Hasura

  • public : SELECT des workflows actifs uniquement (status = 'active')
  • public : workflow_steps visibles uniquement si le workflow parent est actif
  • admin : CRUD complet sur toutes les lignes (y compris drafts)

Composants UI

WorkflowCard

// src/components/workflows/WorkflowCard.tsx
import { Clock } from 'lucide-react';
import { getIconComponent } from '@/utils/icons';

export function WorkflowCard({ workflow, onClick }: WorkflowCardProps) {
  const Icon = getIconComponent(workflow.icon);

  const difficultyColors = {
    'débutant': 'bg-green-100 text-green-800',
    'intermédiaire': 'bg-orange-100 text-orange-800',
    'expert': 'bg-red-100 text-red-800',
  };

  return (
    <div onClick={onClick} className="p-6 border rounded-lg hover:shadow-lg transition cursor-pointer">
      <div className="flex items-start gap-4">
        <div className="p-3 bg-indigo-100 rounded-lg">
          <Icon className="w-6 h-6 text-indigo-600" />
        </div>
        <div className="flex-1">
          <h3 className="font-semibold text-lg mb-2">{workflow.title}</h3>
          <p className="text-gray-600 text-sm mb-4">{workflow.description}</p>
          <div className="flex items-center gap-4 text-sm">
            <span className={`px-2 py-1 rounded ${difficultyColors[workflow.difficulty]}`}>
              {workflow.difficulty}
            </span>
            <span className="flex items-center gap-1 text-gray-500">
              <Clock className="w-4 h-4" /> {workflow.duration}
            </span>
          </div>
        </div>
      </div>
    </div>
  );
}

WorkflowStepper

// src/components/workflows/WorkflowStepper.tsx
export function WorkflowStepper({ steps, currentStep, onStepClick }) {
  return (
    <div className="flex items-center justify-between mb-8">
      {steps.map((step, index) => {
        const isActive = currentStep === index;
        const isCompleted = currentStep > index;

        return (
          <div key={step.id} className="flex items-center flex-1">
            <button
              onClick={() => onStepClick(index)}
              className={`w-10 h-10 rounded-full flex items-center justify-center
                ${isCompleted ? 'bg-green-500 text-white' :
                  isActive ? 'bg-indigo-600 text-white' :
                  'bg-gray-200 text-gray-600'}`}
            >
              {isCompleted ? '✓' : index + 1}
            </button>
            {index < steps.length - 1 && (
              <div className={`h-1 flex-1 mx-2 ${isCompleted ? 'bg-green-500' : 'bg-gray-200'}`} />
            )}
          </div>
        );
      })}
    </div>
  );
}

Hooks

useWorkflowProgress

// src/features/workflows/hooks/useWorkflowProgress.ts
export function useWorkflowProgress(totalSteps: number) {
  const [currentStep, setCurrentStep] = useState(0);
  const [completedSteps, setCompletedSteps] = useState<Set<number>>(new Set());

  const nextStep = () => { /* ... */ };
  const prevStep = () => { /* ... */ };
  const goToStep = (step: number) => { /* ... */ };
  const progress = (completedSteps.size / totalSteps) * 100;

  return {
    currentStep, completedSteps, progress,
    nextStep, prevStep, goToStep,
    isFirstStep: currentStep === 0,
    isLastStep: currentStep === totalSteps - 1,
  };
}

Bonnes pratiques

✅ À faire

  • Instructions claires et actionnables (un objectif par étape)
  • 3-7 étapes max par workflow
  • Durée réaliste (tester avant de publier)
  • Checklist de validation par étape

❌ À éviter

  • Instructions vagues (“Configurez l’outil”)
  • Étapes trop longues (> 15 min)
  • Oubli des prérequis
  • Jargon technique sans explication

Ressources

Database Schema

Structure des tables workflows

API Layer

Utiliser l’API GraphQL

Admin Dashboard

Créer et gérer des workflows

Patterns

Modals, forms, hooks