MedReport Logo

MedReport API

Documentation

Documentation API MedReport

Documentation complète pour l'intégration Flutter

🔐 Authentification

Toutes les requêtes API doivent inclure un token Bearer dans l'en-tête Authorization :

final response = await http.get(
  Uri.parse('${baseUrl}/api/endpoint'),
  headers: {
    'Authorization': 'Bearer $token',
    'Accept': 'application/json',
  },
);

Inscription

Créer un compte

POST

Endpoint: /api/register

Corps de la requête:

{
    "name": "John Doe",
    "email": "john@example.com",
    "password": "password123",
    "password_confirmation": "password123",
    "birth_date": "1990-01-01",     // Requis
    "phone": "+237600000000",       // Optionnel
    "address": "Yaoundé, Cameroun", // Optionnel
    "speciality": "Cardiologie",    // Optionnel
    "medical_order_number": "12345" // Optionnel
}

Exemple de réponse:

{
    "success": true,
    "message": "Utilisateur créé avec succès",
    "user": {
        "id": 1,
        "uuid": "550e8400-e29b-41d4-a716-446655440000",
        "name": "John Doe",
        "email": "john@example.com",
        "phone": "+237600000000",
        "address": "Yaoundé, Cameroun",
        "birth_date": "1990-01-01",
        "speciality": "Cardiologie",
        "medical_order_number": "12345",
        "role": null,
        "organization_id": null,
        "has_used_trial": false,
        "avatar": null,
        "email_verified_at": null,
        "created_at": "2024-02-14T12:00:00.000000Z",
        "updated_at": "2024-02-14T12:00:00.000000Z"
    },
    "token": "votre_token_d_acces"
}

Exemple d'intégration Flutter:

Future register({
  required String name,
  required String email,
  required String password,
  required String passwordConfirmation,
  String? birthDate,
  String? phone,
  String? address,
  String? speciality,
  String? medicalOrderNumber,
}) async {
  final response = await http.post(
    Uri.parse('${baseUrl}/api/register'),
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json',
    },
    body: jsonEncode({
      'name': name,
      'email': email,
      'password': password,
      'password_confirmation': passwordConfirmation,
      if (birthDate != null) 'birth_date': birthDate,
      if (phone != null) 'phone': phone,
      if (address != null) 'address': address,
      if (speciality != null) 'speciality': speciality,
      if (medicalOrderNumber != null) 'medical_order_number': medicalOrderNumber,
    }),
  );

  if (response.statusCode == 201) {
    final data = jsonDecode(response.body);
    await storage.write(key: 'token', value: data['token']);
    return User.fromJson(data['user']);
  }

  final errorData = jsonDecode(response.body);
  throw Exception(errorData['message'] ?? 'Erreur lors de l\'inscription');
}

Modèles

Modèle User

class User {
  final int id;
  final String uuid;
  final String name;
  final String email;
  final String? phone;
  final String? address;
  final String? speciality;
  final String? medicalOrderNumber;
  final DateTime? birthDate;
  final DateTime? emailVerifiedAt;
  final String? avatar;
  final String? role; // 'owner', 'doctor', 'admin'
  final int? organizationId;
  final bool hasUsedTrial;
  final DateTime createdAt;
  final DateTime updatedAt;

  User({
    required this.id,
    required this.uuid,
    required this.name,
    required this.email,
    this.phone,
    this.address,
    this.speciality,
    this.medicalOrderNumber,
    this.birthDate,
    this.emailVerifiedAt,
    this.avatar,
    this.role,
    this.organizationId,
    this.hasUsedTrial = false,
    required this.createdAt,
    required this.updatedAt,
  });

  factory User.fromJson(Map json) {
    return User(
      id: json['id'],
      uuid: json['uuid'] ?? '',
      name: json['name'],
      email: json['email'],
      phone: json['phone'],
      address: json['address'],
      speciality: json['speciality'],
      medicalOrderNumber: json['medical_order_number'],
      birthDate: json['birth_date'] != null
          ? DateTime.parse(json['birth_date'])
          : null,
      emailVerifiedAt: json['email_verified_at'] != null
          ? DateTime.parse(json['email_verified_at'])
          : null,
      avatar: json['avatar'],
      role: json['role'],
      organizationId: json['organization_id'],
      hasUsedTrial: json['has_used_trial'] ?? false,
      createdAt: DateTime.parse(json['created_at']),
      updatedAt: DateTime.parse(json['updated_at']),
    );
  }

  Map toJson() {
    return {
      'id': id,
      'uuid': uuid,
      'name': name,
      'email': email,
      'phone': phone,
      'address': address,
      'speciality': speciality,
      'medical_order_number': medicalOrderNumber,
      'birth_date': birthDate?.toIso8601String(),
      'email_verified_at': emailVerifiedAt?.toIso8601String(),
      'avatar': avatar,
      'role': role,
      'organization_id': organizationId,
      'has_used_trial': hasUsedTrial,
      'created_at': createdAt.toIso8601String(),
      'updated_at': updatedAt.toIso8601String(),
    };
  }
}

Provider User

class UserProvider extends ChangeNotifier {
  User? _user;
  final storage = const FlutterSecureStorage();

  User? get user => _user;

  Future loadUser() async {
    final token = await storage.read(key: 'token');
    if (token == null) return;

    try {
      final response = await http.get(
        Uri.parse('${baseUrl}/api/me'),
        headers: {
          'Authorization': 'Bearer $token',
          'Accept': 'application/json',
        },
      );

      if (response.statusCode == 200) {
        final data = jsonDecode(response.body);
        _user = User.fromJson(data['user']);
        notifyListeners();
      }
    } catch (e) {
      await storage.delete(key: 'token');
    }
  }

  Future updateProfile({
    String? name,
    String? phone,
    String? address,
    String? birthDate,
    String? speciality,
    String? medicalOrderNumber,
  }) async {
    final token = await storage.read(key: 'token');
    if (token == null) return;

    final response = await http.put(
      Uri.parse('${baseUrl}/api/user/profile'),
      headers: {
        'Authorization': 'Bearer $token',
        'Accept': 'application/json',
        'Content-Type': 'application/json',
      },
      body: jsonEncode({
        if (name != null) 'name': name,
        if (phone != null) 'phone': phone,
        if (address != null) 'address': address,
        if (birthDate != null) 'birth_date': birthDate,
        if (speciality != null) 'speciality': speciality,
        if (medicalOrderNumber != null) 'medical_order_number': medicalOrderNumber,
      }),
    );

    if (response.statusCode == 200) {
      _user = User.fromJson(jsonDecode(response.body));
      notifyListeners();
    } else {
      throw Exception('Échec de la mise à jour du profil');
    }
  }
}

Récupérer l'utilisateur

Récupérer l'utilisateur connecté

GET

Endpoint: /api/user

Note: Pour obtenir l'utilisateur avec son organisation et son rôle, utilisez plutôt /api/me.

Exemple de réponse:

{
    "id": 1,
    "uuid": "550e8400-e29b-41d4-a716-446655440000",
    "name": "John Doe",
    "email": "john@example.com",
    "role": "owner",
    "organization_id": 5,
    "has_used_trial": false,
    "created_at": "2024-02-14T12:00:00.000000Z",
    "updated_at": "2024-02-14T12:00:00.000000Z"
}

Refresh Token

Rafraîchir le token

POST

Endpoint: /api/auth/refresh

Permet de rafraîchir le token d'authentification sans nécessiter une nouvelle connexion.

Exemple de réponse:

{
    "token": "nouveau_token_d_acces"
}

Connexion

Se connecter

POST

Endpoint: /api/login

Corps de la requête:

{
    "email": "john@example.com",
    "password": "password123"
}

Exemple de réponse:

{
    "success": true,
    "message": "Connexion réussie",
    "token": "votre_token_d_acces",
    "user": {
        "id": 1,
        "uuid": "550e8400-e29b-41d4-a716-446655440000",
        "name": "John Doe",
        "email": "john@example.com",
        "role": "owner",
        "organization_id": 5,
        "has_used_trial": false,
        "created_at": "2024-02-14T12:00:00.000000Z",
        "updated_at": "2024-02-14T12:00:00.000000Z"
    }
}

Exemple d'intégration Flutter:

Future login({
  required String email,
  required String password,
}) async {
  final response = await http.post(
    Uri.parse('${baseUrl}/api/login'),
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json',
    },
    body: jsonEncode({
      'email': email,
      'password': password,
    }),
  );

  if (response.statusCode == 200) {
    final data = jsonDecode(response.body);
    // Sauvegarder le token
    await storage.write(key: 'token', value: data['token']);
    return User.fromJson(data['user']);
  }

  final errorData = jsonDecode(response.body);
  throw Exception(errorData['message'] ?? 'Erreur lors de la connexion');
}

Mot de passe oublié

Demander un lien de réinitialisation

POST

Endpoint: /api/forgot-password

Corps de la requête:

{
    "email": "john@example.com"
}

Exemple d'intégration Flutter:

Future forgotPassword({
  required String email,
}) async {
  final response = await http.post(
    Uri.parse('${baseUrl}/api/forgot-password'),
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json',
    },
    body: jsonEncode({
      'email': email,
    }),
  );

  if (response.statusCode != 200) {
    throw Exception(jsonDecode(response.body)['message']);
  }
}

Réinitialiser le mot de passe

POST

Endpoint: /api/reset-password

Corps de la requête:

{
    "token": "token_reçu_par_email",
    "email": "john@example.com",
    "password": "nouveau_mot_de_passe",
    "password_confirmation": "nouveau_mot_de_passe"
}

Exemple d'intégration Flutter:

Future resetPassword({
  required String token,
  required String email,
  required String password,
  required String passwordConfirmation,
}) async {
  final response = await http.post(
    Uri.parse('${baseUrl}/api/reset-password'),
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json',
    },
    body: jsonEncode({
      'token': token,
      'email': email,
      'password': password,
      'password_confirmation': passwordConfirmation,
    }),
  );

  if (response.statusCode != 200) {
    throw Exception(jsonDecode(response.body)['message']);
  }
}

Déconnexion

Se déconnecter

POST

Endpoint: /api/logout

Exemple de réponse:

{
    "success": true,
    "message": "Déconnexion réussie"
}

Exemple d'intégration Flutter:

Future logout() async {
  final token = await storage.read(key: 'token');
  if (token == null) return;

  final response = await http.post(
    Uri.parse('${baseUrl}/api/logout'),
    headers: {
      'Authorization': 'Bearer $token',
      'Accept': 'application/json',
    },
  );

  if (response.statusCode == 200) {
    // Supprimer le token stocké
    await storage.delete(key: 'token');
    return;
  }

  final errorData = jsonDecode(response.body);
  throw Exception(errorData['message'] ?? 'Erreur lors de la déconnexion');
}

Authentification OAuth

Les routes OAuth utilisent un format générique avec le provider en paramètre :

  • Redirection: /api/auth/{provider} (ex: /api/auth/google)
  • Callback: /api/auth/{provider}/callback (ex: /api/auth/google/callback)

Providers supportés: google, facebook, github

Connexion avec Google

GET

URL de redirection: /api/auth/google

URL de callback: /api/auth/google/callback

Le callback retourne un token et les informations utilisateur après authentification réussie.

Future signInWithGoogle() async {
  final url = Uri.parse('${baseUrl}/api/auth/google');
  // Utiliser url_launcher ou webview pour la redirection
  if (await canLaunch(url.toString())) {
    await launch(url.toString());
  } else {
    throw Exception('Impossible d\'ouvrir l\'URL d\'authentification');
  }
}

Connexion avec Facebook

GET

URL de redirection: /api/auth/facebook

URL de callback: /api/auth/facebook/callback

Future signInWithFacebook() async {
  final url = Uri.parse('${baseUrl}/api/auth/facebook');
  if (await canLaunch(url.toString())) {
    await launch(url.toString());
  } else {
    throw Exception('Impossible d\'ouvrir l\'URL d\'authentification');
  }
}

Connexion avec GitHub

GET

URL de redirection: /api/auth/github

URL de callback: /api/auth/github/callback

Future signInWithGithub() async {
  final url = Uri.parse('${baseUrl}/api/auth/github');
  if (await canLaunch(url.toString())) {
    await launch(url.toString());
  } else {
    throw Exception('Impossible d\'ouvrir l\'URL d\'authentification');
  }
}

🏢 Multi-tenant

Créer organisation + owner

POST

Endpoint: /api/register-owner

Corps de la requête:

{
  "name": "Owner One",
  "email": "owner@example.com",
  "password": "password123",
  "password_confirmation": "password123",
  "organization_name": "Clinique Alpha",
  "birth_date": "1990-01-01",     // Optionnel
  "phone": "+237600000000",       // Optionnel
  "address": "Yaoundé, Cameroun", // Optionnel
  "speciality": "Cardiologie",    // Optionnel
  "medical_order_number": "12345" // Optionnel
}

Réponse:

{
  "user": {
    "id": 1,
    "uuid": "550e8400-e29b-41d4-a716-446655440000",
    "name": "Owner One",
    "email": "owner@example.com",
    "role": "owner",
    "organization_id": 5,
    "has_used_trial": false,
    "created_at": "2024-02-14T12:00:00.000000Z",
    "updated_at": "2024-02-14T12:00:00.000000Z"
  },
  "token": "votre_token_d_acces",
  "organization": {
    "id": 5,
    "uuid": "660e8400-e29b-41d4-a716-446655440001",
    "name": "Clinique Alpha",
    "code": "ORG-XXXX",
    "owner_user_id": 1,
    "created_at": "2024-02-14T12:00:00.000000Z",
    "updated_at": "2024-02-14T12:00:00.000000Z"
  },
  "organization_code": "ORG-XXXX"
}

Exemple d'intégration Flutter:

Future> registerOwner({
  required String name,
  required String email,
  required String password,
  required String passwordConfirmation,
  required String organizationName,
  String? birthDate,
  String? phone,
  String? address,
  String? speciality,
  String? medicalOrderNumber,
}) async {
  final response = await http.post(
    Uri.parse('${baseUrl}/api/register-owner'),
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json',
    },
    body: jsonEncode({
      'name': name,
      'email': email,
      'password': password,
      'password_confirmation': passwordConfirmation,
      'organization_name': organizationName,
      if (birthDate != null) 'birth_date': birthDate,
      if (phone != null) 'phone': phone,
      if (address != null) 'address': address,
      if (speciality != null) 'speciality': speciality,
      if (medicalOrderNumber != null) 'medical_order_number': medicalOrderNumber,
    }),
  );

  if (response.statusCode == 201) {
    final data = jsonDecode(response.body);
    await storage.write(key: 'token', value: data['token']);
    return data;
  }

  throw Exception(jsonDecode(response.body)['error'] ?? 'Erreur lors de l\'inscription');
}

Inscrire un médecin via code

POST

Endpoint: /api/register-doctor

Corps de la requête:

{
  "name": "Dr Jane",
  "email": "drjane@example.com",
  "password": "password123",
  "password_confirmation": "password123",
  "organization_code": "ORG-XXXX",
  "birth_date": "1990-01-01",     // Optionnel
  "phone": "+237600000000",       // Optionnel
  "address": "Yaoundé, Cameroun", // Optionnel
  "speciality": "Cardiologie",    // Optionnel
  "medical_order_number": "12345" // Optionnel
}

Réponse:

{
  "user": {
    "id": 2,
    "uuid": "550e8400-e29b-41d4-a716-446655440001",
    "name": "Dr Jane",
    "email": "drjane@example.com",
    "role": "doctor",
    "organization_id": 5,
    "has_used_trial": false,
    "created_at": "2024-02-14T12:00:00.000000Z",
    "updated_at": "2024-02-14T12:00:00.000000Z"
  },
  "token": "votre_token_d_acces",
  "organization": {
    "id": 5,
    "uuid": "660e8400-e29b-41d4-a716-446655440001",
    "name": "Clinique Alpha",
    "code": "ORG-XXXX",
    "owner_user_id": 1,
    "created_at": "2024-02-14T12:00:00.000000Z",
    "updated_at": "2024-02-14T12:00:00.000000Z"
  }
}

Exemple d'intégration Flutter:

Future> registerDoctor({
  required String name,
  required String email,
  required String password,
  required String passwordConfirmation,
  required String organizationCode,
  String? birthDate,
  String? phone,
  String? address,
  String? speciality,
  String? medicalOrderNumber,
}) async {
  final response = await http.post(
    Uri.parse('${baseUrl}/api/register-doctor'),
    headers: {
      'Accept': 'application/json',
      'Content-Type': 'application/json',
    },
    body: jsonEncode({
      'name': name,
      'email': email,
      'password': password,
      'password_confirmation': passwordConfirmation,
      'organization_code': organizationCode,
      if (birthDate != null) 'birth_date': birthDate,
      if (phone != null) 'phone': phone,
      if (address != null) 'address': address,
      if (speciality != null) 'speciality': speciality,
      if (medicalOrderNumber != null) 'medical_order_number': medicalOrderNumber,
    }),
  );

  if (response.statusCode == 201) {
    final data = jsonDecode(response.body);
    await storage.write(key: 'token', value: data['token']);
    return data;
  }

  final errorData = jsonDecode(response.body);
  throw Exception(errorData['error'] ?? 'Code d\'organisation invalide');
}

Contexte utilisateur

GET

Endpoint: /api/me

Récupère l'utilisateur connecté avec son organisation et son rôle.

Réponse:

{
  "user": {
    "id": 1,
    "uuid": "550e8400-e29b-41d4-a716-446655440000",
    "name": "Owner One",
    "email": "owner@example.com",
    "role": "owner",
    "organization_id": 5,
    "has_used_trial": false,
    "created_at": "2024-02-14T12:00:00.000000Z",
    "updated_at": "2024-02-14T12:00:00.000000Z"
  },
  "organization": {
    "id": 5,
    "uuid": "660e8400-e29b-41d4-a716-446655440001",
    "name": "Clinique Alpha",
    "code": "ORG-XXXX",
    "owner_user_id": 1,
    "created_at": "2024-02-14T12:00:00.000000Z",
    "updated_at": "2024-02-14T12:00:00.000000Z"
  },
  "role": "owner"
}

Exemple d'intégration Flutter:

Future> getMe() async {
  final token = await storage.read(key: 'token');
  if (token == null) throw Exception('Non authentifié');

  final response = await http.get(
    Uri.parse('${baseUrl}/api/me'),
    headers: {
      'Authorization': 'Bearer $token',
      'Accept': 'application/json',
    },
  );

  if (response.statusCode == 200) {
    return jsonDecode(response.body);
  }

  throw Exception('Erreur lors de la récupération du contexte');
}

🏢 Organisation

Détails de l'organisation

GET

Endpoint: /api/organization

Exemple de réponse:

{
    "organization": {
        "id": 1,
        "uuid": "660e8400-e29b-41d4-a716-446655440001",
        "name": "Clinique Alpha",
        "code": "ORG-XXXX",
        "owner_user_id": 1,
        "created_at": "2024-02-14T12:00:00.000000Z",
        "updated_at": "2024-02-14T12:00:00.000000Z"
    },
    "role": "owner"
}

Exemple d'intégration Flutter:

Future> getOrganization() async {
  final token = await storage.read(key: 'token');
  if (token == null) throw Exception('Non authentifié');

  final response = await http.get(
    Uri.parse('${baseUrl}/api/organization'),
    headers: {
      'Authorization': 'Bearer $token',
      'Accept': 'application/json',
    },
  );

  if (response.statusCode == 200) {
    return jsonDecode(response.body);
  }

  throw Exception('Erreur lors de la récupération de l\'organisation');
}

Mettre à jour l'organisation

PUT

Endpoint: /api/organization

Corps de la requête:

{
    "name": "Nouveau Nom de la Clinique"
}

Réponse:

{
    "organization": {
        "id": 1,
        "uuid": "660e8400-e29b-41d4-a716-446655440001",
        "name": "Nouveau Nom de la Clinique",
        "code": "ORG-XXXX",
        "owner_user_id": 1,
        "created_at": "2024-02-14T12:00:00.000000Z",
        "updated_at": "2024-02-14T13:00:00.000000Z"
    },
    "message": "Organisation mise à jour avec succès"
}

Exemple d'intégration Flutter:

Future> updateOrganization({
  required String name,
}) async {
  final token = await storage.read(key: 'token');
  if (token == null) throw Exception('Non authentifié');

  final response = await http.put(
    Uri.parse('${baseUrl}/api/organization'),
    headers: {
      'Authorization': 'Bearer $token',
      'Accept': 'application/json',
      'Content-Type': 'application/json',
    },
    body: jsonEncode({
      'name': name,
    }),
  );

  if (response.statusCode == 200) {
    return jsonDecode(response.body);
  }

  throw Exception('Erreur lors de la mise à jour de l\'organisation');
}

Pivoter le code d'invitation

POST

Endpoint: /api/organization/rotate-code

Réponse:

{
    "code": "ORG-YYYY",
    "message": "Code d'invitation pivoté avec succès"
}

Exemple d'intégration Flutter:

Future rotateOrganizationCode() async {
  final token = await storage.read(key: 'token');
  if (token == null) throw Exception('Non authentifié');

  final response = await http.post(
    Uri.parse('${baseUrl}/api/organization/rotate-code'),
    headers: {
      'Authorization': 'Bearer $token',
      'Accept': 'application/json',
    },
  );

  if (response.statusCode == 200) {
    final data = jsonDecode(response.body);
    return data['code'];
  }

  throw Exception('Erreur lors de la rotation du code');
}

📈 Rapports & Logs

Aperçu des rapports

GET

Endpoint: /api/reports/overview

Réponse:

{
    "patients": {
        "count": 120,
        "recent": [
            { "uuid": "...", "name": "Patient Récent 1", "created_at": "..." }
        ]
    },
    "prescriptions": { "count": 350 },
    "invoices": { "count": 280 }
}

Journal d'activité

GET

Endpoint: /api/activity-log

Paramètres de filtre (optionnels): from, to, user_id, action_type, entity_type, search, limit, page.

Réponse:

{
    "data": [
        {
            "id": 1,
            "timestamp": "...",
            "user_name": "Dr. John",
            "action": "patient.created",
            "entity_type": "patient",
            "entity_uuid": "...",
            "entity_name": "Nom du Patient"
        }
    ],
    "total": 50,
    "page": 1,
    "limit": 25
}

🛡️ Administration

Statistiques du tableau de bord

GET

Endpoint: /api/admin/dashboard/stats

Réponse:

{
    "stats": [ ... ],
    "charts": {
        "userTrend": [ ... ]
    },
    "recentActivities": [ ... ]
}

Gestion des Utilisateurs (Admin)

Lister: GET /api/admin/users

Détail: GET /api/admin/users/{id}

Mettre à jour: PUT /api/admin/users/{id}

Changer Statut: PATCH /api/admin/users/{id}/status

Supprimer: DELETE /api/admin/users/{id}

📁 CRUD Métier

Patients

GET

Liste paginée: /api/patients

Créer: POST /api/patients

Détail: GET /api/patients/{uuid}

MàJ: PATCH /api/patients/{uuid}

Suppression: DELETE /api/patients/{uuid}

Export: GET /api/patients/export?format=csv

Prescriptions

GET

Base: /api/prescriptions (CRUD REST)

Export: GET /api/prescriptions/export?format=csv

{
  "uuid":"...",
  "patient_uuid":"...",
  "doctor_uuid":"...",
  "medication":"Paracetamol 500mg",
  "dosage":"3x/jour",
  "duration":"7 jours",
  "notes": "Après repas",
  "validity_period": 30,
  "file_uuid": null,
  "date_issued":"2025-01-02"
}

Factures

GET

Base: /api/invoices (CRUD REST)

{
  "uuid":"...",
  "patient_uuid":"...",
  "doctor_uuid":"...",
  "designation":"Consultation",
  "unit_price":15000,
  "quantity":1,
  "total_amount":15000,
  "file_uuid": null,
  "date_issued":"2025-01-02"
}

📎 Fichiers

Upload

POST

Endpoint: /api/files (multipart/form-data)

file=@/path/to/doc.pdf
owner_type=patient|prescription|invoice
owner_uuid=...

Réponse:

{
  "uuid": "...",
  "path": "org_5/2025/01/....pdf",
  "original_name": "ordonnance.pdf",
  "mime_type": "application/pdf",
  "size": 245678,
  "checksum": "...",
  "file_version": 1,
  "created_at": "2025-01-02T16:00:00Z"
}

Téléchargement

GET

Endpoint: /api/files/{uuid}

🔄 Synchronisation

Pull changes

GET

Endpoint: /api/sync/changes?since=ISO8601&entities[]=patients&entities[]=prescriptions...

{
  "cursor": "2025-01-02T15:30:45Z",
  "changes": {
    "patients": [{"uuid":"...","action":"upsert","data":{...}}],
    "prescriptions": [],
    "invoices": [],
    "files": []
  }
}

Push batch (idempotent)

POST

Endpoint: /api/sync/batch (≤100 éléments/entité)

{
  "patients": [{"uuid":"...","name":"Jean","updated_at":"2025-01-02T11:00:00Z"}],
  "prescriptions": [],
  "invoices": [],
  "files": []
}

Réponse:

{
  "cursor": "2025-01-02T15:45:30Z",
  "results": {"patients": [{"uuid":"...","status":"ok","updated_at":"..."}]},
  "errors": []
}

Conflits critiques → listés dans errors (ex: organization_mismatch).

Statut de la synchronisation

GET

Endpoint: /api/sync/status

Réponse:

{
    "pending_count": 12,
    "error_count": 1,
    "last_sync_at": "2025-01-02T15:45:30Z",
    "errors": [
        {
            "timestamp": "2025-01-02T15:40:00Z",
            "message": "Erreur de synchronisation",
            "metadata": { ... }
        }
    ]
}

⏱️ Rate limiting

  • /api/login: 5/min
  • /api/register*: 3/h
  • /api/sync/*: 60/h
  • /api/files (upload): 20/h
  • Groupe API protégé: 120/min

👤 Profil Utilisateur

Récupérer le profil

GET

Endpoint: /api/user/profile

Paramètres: Aucun

Exemple de réponse:

{
    "user": {
        "id": 1,
        "uuid": "550e8400-e29b-41d4-a716-446655440000",
        "name": "John Doe",
        "email": "john@example.com",
        "avatar": "1_1743615420.jpg",
        "phone": "+237600000000",
        "address": "Yaoundé, Cameroun",
        "birth_date": "1990-01-01",
        "speciality": "Cardiologie",
        "medical_order_number": "12345",
        "role": "owner",
        "organization_id": 5,
        "has_used_trial": true,
        "email_verified_at": "2024-02-14T12:00:00.000000Z",
        "created_at": "2024-02-14T12:00:00.000000Z",
        "updated_at": "2024-02-14T12:00:00.000000Z"
    },
    "message": "Profil récupéré avec succès"
}

Mettre à jour le profil

PUT

Endpoint: /api/user/profile

Corps de la requête (multipart/form-data):

{
    "name": "John Doe",           // Optionnel
    "birth_date": "1990-01-01",   // Optionnel
    "phone": "+237600000000",     // Optionnel
    "address": "Yaoundé, Cameroun", // Optionnel
    "speciality": "Cardiologie",  // Optionnel
    "medical_order_number": "12345", // Optionnel
    "avatar": [FICHIER IMAGE]     // Optionnel, max 2MB
}

Exemple de réponse:

{
    "message": "Profil mis à jour avec succès",
    "user": {
        "id": 1,
        "uuid": "550e8400-e29b-41d4-a716-446655440000",
        "name": "John Doe",
        "email": "john@example.com",
        "avatar": "1_1743615420.jpg",
        "phone": "+237600000000",
        "address": "Yaoundé, Cameroun",
        "birth_date": "1990-01-01",
        "speciality": "Cardiologie",
        "medical_order_number": "12345",
        "role": "owner",
        "organization_id": 5,
        "has_used_trial": true,
        "email_verified_at": "2024-02-14T12:00:00.000000Z",
        "created_at": "2024-02-14T12:00:00.000000Z",
        "updated_at": "2024-02-14T12:00:00.000000Z"
    }
}

Exemple d'intégration Flutter (avec upload d'avatar):

Future updateProfile({
  String? name,
  String? phone,
  String? address,
  String? birthDate,
  String? speciality,
  String? medicalOrderNumber,
  File? avatarFile,
}) async {
  final token = await storage.read(key: 'token');
  if (token == null) throw Exception('Non authentifié');

  final request = http.MultipartRequest(
    'PUT',
    Uri.parse('${baseUrl}/api/user/profile'),
  );

  request.headers.addAll({
    'Authorization': 'Bearer $token',
    'Accept': 'application/json',
  });

  if (name != null) request.fields['name'] = name;
  if (phone != null) request.fields['phone'] = phone;
  if (address != null) request.fields['address'] = address;
  if (birthDate != null) request.fields['birth_date'] = birthDate;
  if (speciality != null) request.fields['speciality'] = speciality;
  if (medicalOrderNumber != null) request.fields['medical_order_number'] = medicalOrderNumber;
  
  if (avatarFile != null) {
    request.files.add(
      await http.MultipartFile.fromPath('avatar', avatarFile.path),
    );
  }

  final streamedResponse = await request.send();
  final response = await http.Response.fromStream(streamedResponse);

  if (response.statusCode == 200) {
    final data = jsonDecode(response.body);
    return User.fromJson(data['user']);
  }

  throw Exception('Échec de la mise à jour du profil');
}

Mettre à jour l'email

PUT

Endpoint: /api/user/profile/email

Corps de la requête:

{
    "email": "nouveau@example.com",
    "current_password": "mot_de_passe_actuel"
}

Exemple de réponse:

{
    "message": "Adresse email mise à jour avec succès",
    "user": {
        "id": 1,
        "uuid": "550e8400-e29b-41d4-a716-446655440000",
        "name": "John Doe",
        "email": "nouveau@example.com",
        "email_verified_at": null,
        "role": "owner",
        "organization_id": 5,
        "has_used_trial": true,
        "created_at": "2024-02-14T12:00:00.000000Z",
        "updated_at": "2024-02-14T12:00:00.000000Z"
    }
}

Exemple d'intégration Flutter:

Future updateEmail({
  required String email,
  required String currentPassword,
}) async {
  final token = await storage.read(key: 'token');
  if (token == null) throw Exception('Non authentifié');

  final response = await http.put(
    Uri.parse('${baseUrl}/api/user/profile/email'),
    headers: {
      'Authorization': 'Bearer $token',
      'Accept': 'application/json',
      'Content-Type': 'application/json',
    },
    body: jsonEncode({
      'email': email,
      'current_password': currentPassword,
    }),
  );

  if (response.statusCode == 200) {
    final data = jsonDecode(response.body);
    return User.fromJson(data['user']);
  }

  throw Exception('Échec de la mise à jour de l\'email');
}

Changer le mot de passe

PUT

Endpoint: /api/user/profile/password

Corps de la requête:

{
    "current_password": "mot_de_passe_actuel",
    "password": "nouveau_mot_de_passe",
    "password_confirmation": "nouveau_mot_de_passe"
}

Exemple de réponse:

{
    "message": "Mot de passe mis à jour avec succès"
}

Supprimer l'avatar

DELETE

Endpoint: /api/user/profile/avatar

Paramètres: Aucun

Exemple de réponse:

{
    "message": "Avatar supprimé avec succès",
    "user": {
        "id": 1,
        "uuid": "550e8400-e29b-41d4-a716-446655440000",
        "name": "John Doe",
        "email": "john@example.com",
        "avatar": null,
        "role": "owner",
        "organization_id": 5,
        "has_used_trial": true,
        "created_at": "2024-02-14T12:00:00.000000Z",
        "updated_at": "2024-02-14T12:00:00.000000Z"
    }
}

Exemple d'intégration Flutter:

Future deleteAvatar() async {
  final token = await storage.read(key: 'token');
  if (token == null) throw Exception('Non authentifié');

  final response = await http.delete(
    Uri.parse('${baseUrl}/api/user/profile/avatar'),
    headers: {
      'Authorization': 'Bearer $token',
      'Accept': 'application/json',
    },
  );

  if (response.statusCode == 200) {
    final data = jsonDecode(response.body);
    return User.fromJson(data['user']);
  }

  throw Exception('Échec de la suppression de l\'avatar');
}

Rejoindre une organisation

POST

Endpoint: /api/user/profile/join-organization

Corps de la requête:

{
    "organization_code": "ORG-XXXX"
}

Exemple de réponse:

{
    "message": "Organisation rejointe avec succès",
    "organization": {
        "id": 5,
        "uuid": "660e8400-e29b-41d4-a716-446655440001",
        "name": "Clinique Alpha",
        "code": "ORG-XXXX",
        "owner_user_id": 1,
        "created_at": "2024-02-14T12:00:00.000000Z",
        "updated_at": "2024-02-14T12:00:00.000000Z"
    },
    "user": {
        "id": 1,
        "uuid": "550e8400-e29b-41d4-a716-446655440000",
        "name": "John Doe",
        "email": "john@example.com",
        "organization_id": 5,
        "role": "doctor",
        "created_at": "2024-02-14T12:00:00.000000Z",
        "updated_at": "2024-02-14T12:00:00.000000Z"
    }
}

Exemple d'intégration Flutter:

Future> joinOrganization({
  required String organizationCode,
}) async {
  final token = await storage.read(key: 'token');
  if (token == null) throw Exception('Non authentifié');

  final response = await http.post(
    Uri.parse('${baseUrl}/api/user/profile/join-organization'),
    headers: {
      'Authorization': 'Bearer $token',
      'Accept': 'application/json',
      'Content-Type': 'application/json',
    },
    body: jsonEncode({
      'organization_code': organizationCode,
    }),
  );

  if (response.statusCode == 200) {
    return jsonDecode(response.body);
  }

  final errorData = jsonDecode(response.body);
  throw Exception(errorData['error'] ?? 'Code d\'organisation invalide');
}

👥 Gestion des Utilisateurs

Gestion des Utilisateurs (Administration)

Liste des Utilisateurs

GET

Endpoint: /api/admin/users

Paramètres:

  • page: Numéro de page (défaut: 1)
  • limit: Éléments par page (défaut: 10)
  • search: Terme de recherche pour nom, email, spécialité ou numéro d'ordre
Future> getUsers({
  int page = 1,
  int limit = 10,
  String? search,
}) async {
  final queryParams = {
    'page': page.toString(),
    'limit': limit.toString(),
    if (search != null) 'search': search,
  };

  final response = await http.get(
    Uri.parse('${baseUrl}/api/admin/users').replace(queryParameters: queryParams),
    headers: {
      'Authorization': 'Bearer $token',
      'Accept': 'application/json',
    },
  );

  if (response.statusCode == 200) {
    return json.decode(response.body);
  }
  throw Exception('Échec du chargement des utilisateurs');
}

Exemple de réponse:

{
  "data": [
    {
      "id": "1",
      "name": "John Doe",
      "email": "john@example.com",
      "status": "active",
      "createdAt": "2024-01-01T00:00:00.000Z",
      "phone": "+237600000000",
      "address": "Yaoundé, Cameroun",
      "speciality": "Cardiologie",
      "medical_order_number": "12345"
    }
  ],
  "total": 50,
  "currentPage": 1,
  "perPage": 10,
  "lastPage": 5
}

Détails d'un Utilisateur

GET

Endpoint: /api/admin/users/{id}

Future> getUserDetails(String userId) async {
  final response = await http.get(
    Uri.parse('${baseUrl}/api/admin/users/$userId'),
    headers: {
      'Authorization': 'Bearer $token',
      'Accept': 'application/json',
    },
  );

  if (response.statusCode == 200) {
    return json.decode(response.body);
  }
  throw Exception('Échec du chargement des détails de l\'utilisateur');
}

Exemple de réponse:

{
  "user": {
    "id": "1",
    "name": "John Doe",
    "email": "john@example.com",
    "phone": "+237600000000",
    "address": "Yaoundé, Cameroun",
    "birth_date": "1990-01-01",
    "speciality": "Cardiologie",
    "medical_order_number": "12345",
    "createdAt": "2024-01-01T00:00:00.000Z",
    "status": "active",
    "roles": ["doctor"]
  }
}

Modèles

Modèle AdminUser

class AdminUser {
  final String id;
  final String name;
  final String email;
  final String? phone;
  final String? address;
  final String? speciality;
  final String? medicalOrderNumber;
  final String status;
  final DateTime createdAt;

  AdminUser({
    required this.id,
    required this.name,
    required this.email,
    this.phone,
    this.address,
    this.speciality,
    this.medicalOrderNumber,
    required this.status,
    required this.createdAt,
  });

  factory AdminUser.fromJson(Map json) {
    return AdminUser(
      id: json['id'].toString(),
      name: json['name'],
      email: json['email'],
      phone: json['phone'],
      address: json['address'],
      speciality: json['speciality'],
      medicalOrderNumber: json['medical_order_number'],
      status: json['status'],
      createdAt: DateTime.parse(json['createdAt']),
    );
  }
}