# milsat_modules_flutter v1.3.0

A comprehensive Flutter package for geospatial data collection, user management, dynamic form rendering, data export/sync, and hierarchical location services.

📦 What is milsat_modules_flutter?

A production-ready Flutter package built with Clean Architecture principles, providing 6 independent yet integrated modules for building sophisticated data collection and geospatial applications.

### 🚀 Key Features

### Common Foundation Module

Core utilities, error handling, network layer, and shared entities.

- Result pattern for error handling
- Automatic token refresh
- Network utilities with Dio
- Configuration management

### User Authentication & Users

Complete user management and authentication system.

- Email & OTP authentication
- Profile management
- Project compendium
- Address verification

### Form Dynamic Forms

Powerful dynamic form builder with validation and conditional logic.

- 8+ form widget types
- Conditional field logic
- Multi-page forms
- Script evaluation

### Data Data Management

Robust data management with local SQLite and cloud sync.

- CRUD operations
- Cloud export & sync
- Validation workflows
- Image synchronization

### Location Hierarchical Locations

Nigerian location services with 6.8MB of CSV data.

- State → LGA → Ward → PU hierarchy
- INEC polling units
- NIPOST localities
- Custom location support

### Map Mapbox Integration

Advanced mapping with geometry drawing and visualization.

- Point, Line, Polygon drawing
- Work area visualization
- Undo/redo support
- Dynamic icons

### 🏗️ Architecture Overview

Built with Feature-first Clean Architecture, each module follows a consistent pattern:

- Domain Layer: Entities, repository interfaces, service interfaces
- Data Layer: Models, data sources, repository implementations
- Presentation Layer: Widgets, UI components, state management

✅ Who Should Use This Package?

This package is ideal for developers building:

- Field data collection applications
- Survey and enumeration systems
- Geospatial data management platforms
- Location-based services in Nigeria
- Enterprise data collection workflows

## Getting Started

Quick start guide to integrate milsat_modules_flutter into your Flutter application.

### 📦 Installation

Add `milsat_modules_flutter` to your `pubspec.yaml`:

    dependencies:
      flutter:
        sdk: flutter
      milsat_modules_flutter:
        git:
          url: https://github.com/HaywhyD/milsat_modules_flutter
          ref: main  # or specific version tag

Then run:

    flutter pub get

### ⚡ Quick Start

Initialize the package in your `main.dart` file:

    import 'package:flutter/material.dart';
    import 'package:milsat_modules_flutter/milsat_modules_flutter.dart';

    void main() async {
      // CRITICAL: Initialize bindings first
      WidgetsFlutterBinding.ensureInitialized();

      // Initialize all modules
      await ModulesConfig.initialize(
        baseUrl: "https://api.yourserver.com/",
        ppqBaseUrl: "https://ppq.yourserver.com/",
        isDebugMode: kDebugMode,
      );

      runApp(const MyApp());
    }

    class MyApp extends StatelessWidget {
      const MyApp({Key? key}) : super(key: key);

      @override
      Widget build(BuildContext context) {
        return MaterialApp(
          title: 'My Data Collection App',
          home: HomeScreen(),
        );
      }
    }

⚠️ Important Setup Steps

1. Always call `WidgetsFlutterBinding.ensureInitialized()` before ModulesConfig
2. Use `await` when calling ModulesConfig.initialize()
3. Initialize BEFORE `runApp()`
4. Provide both `baseUrl` and `ppqBaseUrl`

### 🔑 Accessing Services

Services are accessed via Config classes:

    import 'package:milsat_modules_flutter/milsat_modules_flutter.dart';

    class MyScreen extends StatefulWidget {
      @override
      _MyScreenState createState() => _MyScreenState();
    }

    class _MyScreenState extends State {
      // Use late final for lazy initialization
      late final UserService _userService = UserConfig.userService;
      late final DataService _dataService = DataConfig.dataService;

      Future _performLogin() async {
        final result = await _userService.loginWithEmail(email, password);

        result.fold(
          onSuccess: (user) {
            // Handle successful login
            print('Welcome ${user.profile.firstName}!');
          },
          onFailure: (error) {
            // Handle error
            print('Login failed: ${error.message}');
          },
        );
      }
    }

### 📋 First Steps Checklist

#### 1️⃣ Setup Configuration

Initialize ModulesConfig with your API endpoints

#### 2️⃣ Implement Authentication

Use UserService for login/logout workflows

#### 3️⃣ Import Survey Config

Use DataService to import form configurations

#### 4️⃣ Build Forms

Use FormBuilder widget for data collection

#### 5️⃣ Manage Data

CRUD operations with DataService

#### 6️⃣ Export & Sync

Sync data to cloud with exportToCloud

## Common Common Module

Foundation module providing shared utilities, error handling, network layer, and configuration management.

### 📚 Overview

The Common module serves as the foundation for all other modules, providing:

- Result Pattern: Type-safe error handling
- Network Layer: Dio-based HTTP client with automatic token refresh
- Error Types: Comprehensive error classification
- Core Entities: Shared data models across modules
- Configuration: Centralized package configuration

### ⚙️ Configuration

`ModulesConfig.initialize({required String baseUrl, required String ppqBaseUrl, bool? isDebugMode, FormConfig? formConfig})`

Initializes all modules with the provided configuration.

##### Parameters:

baseUrl String Required

Main API endpoint URL

ppqBaseUrl String Required

PPQ (Plant Protection & Quarantine) API endpoint

isDebugMode bool?

Enable debug logging (defaults to false)

formConfig FormConfig?

Optional form widget customization

### ✅ Result Pattern

All service methods return a `Result<T>` type for type-safe error handling:

    // Result has two states: Success or Failure
    final result = await userService.loginWithEmail(email, password);

    // Use fold() to handle both cases
    result.fold(
      onSuccess: (CurrentUserEntity user) {
        // Success - use the data
        print('Logged in as: ${user.profile.firstName}');
        Navigator.push(context, MaterialPageRoute(builder: (_) => Dashboard()));
      },
      onFailure: (AppError error) {
        // Failure - handle the error
        print('Error: ${error.message}');
        showErrorDialog(error.message);
      },
    );

    // Alternative: Check result directly
    if (result.wasSuccessful) {
      final user = result.data!;
      // Use user data
    } else {
      final error = result.errorOrNull;
      // Handle error
    }

### 🚨 Error Types

Error Type | When It Occurs | Common Causes
--- | --- | ---
`NetworkError` | HTTP/network failures | No internet, timeout, server down
`AuthError` | Authentication failures | Invalid credentials, token expired
`UnauthorizedError` | Session expired | Token expired, user logged out
`ValidationError` | Input validation failed | Invalid data format, missing fields
`ServerError` | Backend errors (5xx) | Server crashes, database errors
`UnknownError` | Unexpected errors | Uncaught exceptions

### 🌐 Network Layer

The network layer provides:

- Automatic Token Refresh: Refreshes tokens 60 seconds before expiry
- Request Logging: Debug mode logs all requests/responses
- Timeout Handling: 30-second default timeout
- Error Normalization: Converts HTTP errors to AppError types

🔐 Token Management

The package automatically:

- Stores access and refresh tokens securely
- Calculates token expiry time
- Refreshes tokens before they expire (60s buffer)
- Retries failed requests after token refresh
- Logs out users if refresh fails

## User User Module

Complete authentication and user management system with profile, client, and project compendium support.

### 🔑 Authentication Methods

#### Login with Email

`Future<Result<CurrentUserEntity>> loginWithEmail(String email, String password)`

Authenticates a user with email and password.

Real-world Example from Test App:

    class LoginScreen extends StatefulWidget {
      @override
      _LoginScreenState createState() => _LoginScreenState();
    }

    class _LoginScreenState extends State {
      late final UserService _userService = UserConfig.userService;
      final _emailController = TextEditingController();
      final _passwordController = TextEditingController();
      final _formKey = GlobalKey();
      bool _isLoading = false;
      String _errorMessage = '';

      Future _login() async {
        if (!_formKey.currentState!.validate()) return;

        setState(() {
          _isLoading = true;
          _errorMessage = '';
        });

        try {
          final result = await _userService.loginWithEmail(
            _emailController.text.trim(),
            _passwordController.text,
          );

          result.fold(
            onSuccess: (currentUser) {
              // Navigate to Main Dashboard
              Navigator.pushReplacement(
                context,
                MaterialPageRoute(builder: (context) => MainMenuDashboard()),
              );
            },
            onFailure: (error) {
              setState(() {
                _errorMessage = error.message;
                _isLoading = false;
              });
            },
          );
        } catch (e) {
          setState(() {
            _errorMessage = 'Login failed: $e';
            _isLoading = false;
          });
        }
      }

      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Form(
            key: _formKey,
            child: Column(
              children: [
                TextFormField(
                  controller: _emailController,
                  decoration: InputDecoration(labelText: 'Email'),
                  validator: (value) => value?.isEmpty ?? true ? 'Required' : null,
                ),
                TextFormField(
                  controller: _passwordController,
                  decoration: InputDecoration(labelText: 'Password'),
                  obscureText: true,
                  validator: (value) => value?.isEmpty ?? true ? 'Required' : null,
                ),
                if (_errorMessage.isNotEmpty)
                  Text(_errorMessage, style: TextStyle(color: Colors.red)),
                ElevatedButton(
                  onPressed: _isLoading ? null : _login,
                  child: _isLoading
                    ? CircularProgressIndicator()
                    : Text('Login'),
                ),
              ],
            ),
          ),
        );
      }
    }

### 👤 Other Authentication Methods

- `loginWithOtp(String phoneNumber, String otp)` - OTP-based login
- `generateOtp(String phoneNumber)` - Generate OTP for user
- `logout()` - Clear session and log out
- `isLoggedIn()` - Check if user is currently logged in
- `validateOtp(String email, String otp)` - Validate OTP code
- `createAccount(CreateAccountModel model)` - Register new account
- `resetPassword(ResetPasswordModel model)` - Reset user password

### 📋 Profile Management

    // Get current logged-in user
    final currentUser = _userService.currentUser;
    print('Welcome ${currentUser.profile.firstName}!');

    // Access user properties
    print('Email: ${currentUser.profile.email}');
    print('Phone: ${currentUser.profile.phoneNumber}');
    print('Client: ${currentUser.client.name}');
    print('Projects: ${currentUser.client.projectCompendiums.length}');

    // Refresh user profile from server
    final result = await _userService.refreshUserProfile();

    // Update local profile
    await _userService.updateLocalUserProfile(updatedProfile);

    // Upload profile photo
    await _userService.uploadUserProfilePhoto(imageFile, token, userId);

### 🏢 Client & Project Compendium

    // Access client information
    final client = currentUser.client;
    print('Client ID: ${client.id}');
    print('Client Name: ${client.name}');

    // Access project compendiums
    final projects = currentUser.client.projectCompendiums;
    for (final project in projects) {
      print('Project: ${project.name}');
      print('Form ID: ${project.formId}');
      print('Frame URL: ${project.frameUrl}'); // For map boundaries
    }

    // Get specific project
    final myProject = projects.firstWhereOrNull(
      (p) => p.formId.contains(surveyId),
    );

✅ CurrentUserEntity Properties

- `userId` - Unique user identifier
- `accessToken` - Authentication token
- `profile` - UserProfileEntity (name, email, phone, etc.)
- `client` - ClientEntity (organization info)

## Form Form Module

Dynamic form rendering engine with validation, conditional logic, and 8+ customizable widget types.

### 🎨 FormBuilder Widget

The core widget for rendering dynamic forms from survey configurations.

    FormBuilder({
      required SurveyFormEntity surveyForm,
      required Map uiFieldValues,
      required Map uiFieldMap,
      required Function(String, dynamic) onUIFieldValueChange,
      required Function(BaseUIField) onUIFieldsChange,
      required Function(FormNavigationState) onNavigationStateChanged,
      required Function(FormNavigationActions) onNavigationActionsChanged,
      GlobalKey? formKey,
      FormBuilderInput? input,
      DataService? dataService,
      Function(bool)? onFormEnded,
      bool applyPadding = true,
      bool fillMaxSize = true,
      String? frameUrl,
      // Widget-specific configurations
      TextFieldConfig? defaultTextFieldConfig,
      DropdownConfig? defaultDropdownConfig,
      DatePickerConfig? defaultDatePickerConfig,
      TimePickerConfig? defaultTimePickerConfig,
      PictureConfig? defaultPictureConfig,
      SignatureConfig? defaultSignatureConfig,
    })

### 🔧 Complete Usage Example (from Test App)

    class AddEntryScreen extends StatefulWidget {
      @override
      _AddEntryScreenState createState() => _AddEntryScreenState();
    }

    class _AddEntryScreenState extends State {
      late final DataService _dataService = DataConfig.dataService;

      // Required state for FormBuilder
      final Map _uiFieldValues = {};
      final Map _uiFieldMap = {};
      final GlobalKey _formKey = GlobalKey();

      SurveyFormEntity? _currentSurvey;
      FormNavigationState? _navigationState;
      FormNavigationActions? _navigationActions;

      @override
      void initState() {
        super.initState();
        _loadSurvey();
      }

      Future _loadSurvey() async {
        _currentSurvey = _dataService.currentSurvey?.forms.first;
        _initializeAllFieldsInMap();
        setState(() {});
      }

      void _initializeAllFieldsInMap() {
        // Initialize all fields to null
        for (final page in _currentSurvey?.pages ?? []) {
          for (final field in page.fields) {
            if (!_uiFieldValues.containsKey(field.columnName)) {
              _uiFieldValues[field.columnName] = null;
            }
          }
        }
      }

      // Callback when field value changes
      void _onUIFieldValuesChanged(String key, dynamic value) {
        _uiFieldValues[key] = value;
        setState(() {});
      }

      // Callback when field object changes
      void _onUIFieldsChange(BaseUIField uiField) {
        final fieldName = uiField.field.columnName;
        _uiFieldMap[fieldName] = uiField;
      }

      // Callback when navigation state changes
      void _onNavigationStateChanged(FormNavigationState state) {
        setState(() {
          _navigationState = state;
        });
      }

      // Callback when navigation actions are available
      void _onNavigationActionsChanged(FormNavigationActions actions) {
        _navigationActions = actions;
      }

      // Save form data
      Future _saveEntry() async {
        final formData = await _navigationActions!.saveForm();
        if (formData != null) {
          // Form is valid - save it
          final result = await _dataService.createEntry(
            formName: _currentSurvey!.formName,
            data: formData,
          );

          if (result.wasSuccessful) {
            Navigator.pop(context);
          }
        } else {
          // Form has validation errors
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text('Please fix all form errors')),
          );
        }
      }

      @override
      Widget build(BuildContext context) {
        if (_currentSurvey == null) {
          return Scaffold(
            body: Center(child: CircularProgressIndicator()),
          );
        }

        return Scaffold(
          appBar: AppBar(title: Text('Add Entry')),
          body: Column(
            children: [
              Expanded(
                child: FormBuilder(
                  surveyForm: _currentSurvey!,
                  uiFieldValues: _uiFieldValues,
                  uiFieldMap: _uiFieldMap,
                  onUIFieldValueChange: _onUIFieldValuesChanged,
                  onUIFieldsChange: _onUIFieldsChange,
                  dataService: _dataService,
                  onFormEnded: (ended) {},
                  formKey: _formKey,
                  onNavigationStateChanged: _onNavigationStateChanged,
                  onNavigationActionsChanged: _onNavigationActionsChanged,
                  applyPadding: true,
                  fillMaxSize: false,
                ),
              ),
              // Navigation controls
              if (_navigationState != null) _buildNavigationControls(),
            ],
          ),
          floatingActionButton: FloatingActionButton(
            onPressed: _saveEntry,
            child: Icon(Icons.save),
          ),
        );
      }

      Widget _buildNavigationControls() {
        return Container(
          padding: const EdgeInsets.all(16),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              // Previous button
              if (!_navigationState!.isFirstPage)
                ElevatedButton(
                  onPressed: () => _navigationActions!.goToPreviousPage(),
                  child: Text('Previous'),
                ),

              // Page indicator
              Text(
                'Page ${_navigationState!.currentPage + 1} of ${_navigationState!.totalPages}',
              ),

              // Next button
              if (!_navigationState!.isLastPage)
                ElevatedButton(
                  onPressed: () async {
                    final success = await _navigationActions!.goToNextPage();
                    if (!success) {
                      ScaffoldMessenger.of(context).showSnackBar(
                        SnackBar(content: Text('Fix errors before continuing')),
                      );
                    }
                  },
                  child: Text('Next'),
                ),
            ],
          ),
        );
      }
    }

### 🎯 Available Form Widgets

Widget | Purpose | Configuration
--- | --- | ---
TextFieldWidget | Text input with validation | TextFieldConfig
DropdownWidget | Enhanced dropdown with search | DropdownConfig
LocationDropdownWidget | Hierarchical location selection | DropdownConfig
DatePickerWidget | Date selection | DatePickerConfig
TimePickerWidget | Time selection | TimePickerConfig
PictureWidget | Camera/gallery image capture | PictureConfig
SignatureWidget | Signature pad | SignatureConfig
MultichoiceWidget | Multi-select options | BaseFieldConfig

🎨 3-Tier Configuration System

Forms can be customized at 3 levels:

1. Default: Built-in widget defaults
2. Type-specific: FormConfig for all widgets of a type
3. Widget-specific: Per-field override via FormBuilder params

### ✅ Form Validation

    // Validate entire form
    final (isValid, errors) = await _navigationActions!.validateForm();

    if (isValid) {
      // Form is valid
      final formData = await _navigationActions!.saveForm();
    } else {
      // Show errors
      print('Validation errors: $errors');
    }

## Data Data Module

Comprehensive data management with SQLite storage, cloud sync, and validation workflows.

### 📥 Config Import

    // Import from project compendium (cloud)
    final result = await _dataService.importConfig(
      currentUser: currentUser,
      projectCompendiumId: selectedProject.id,
      localConfigFile: null,
      onProgressUpdate: (message) {
        print('Progress: $message');
        // Messages: "Downloading...", "Validating...", "Parsing...", "Complete"
      },
    );

    result.fold(
      onSuccess: (_) {
        // Access imported survey
        final survey = _dataService.currentSurvey;
        print('Imported: ${survey?.name}');
      },
      onFailure: (error) {
        print('Import failed: ${error.message}');
      },
    );

    // Import from local file
    final file = File('/path/to/config.json');
    await _dataService.importConfig(
      currentUser: currentUser,
      projectCompendiumId: 0,
      localConfigFile: file,
      onProgressUpdate: (message) => print(message),
    );

### ✏️ CRUD Operations

#### Create Entry

    final result = await _dataService.createEntry(
      formName: "survey_form",
      data: {
        'name': 'John Doe',
        'age': 25,
        'location': 'Lagos',
        // ... other field values
      },
      status: EntryStatus.draft, // Optional
      geometry: '{"type": "Point", "coordinates": [3.4, 6.5]}', // Optional
    );

    if (result.wasSuccessful) {
      final entry = result.data!;
      print('Entry created with ID: ${entry.id}');
    }

#### Read Entries

    // Get all entries for a form
    final result = await _dataService.getAllEntries(formName: "survey_form");

    result.fold(
      onSuccess: (List entries) {
        print('Found ${entries.length} entries');
        for (final entry in entries) {
          print('ID: ${entry.id}');
          print('Status: ${entry.status}');
          print('Data: ${entry.properties}'); // Map
          print('Created by: ${entry.constants.userName}');
          print('Location: ${entry.constants.userLat}, ${entry.constants.userLong}');
        }
      },
      onFailure: (error) => print('Error: ${error.message}'),
    );

    // Get entry count
    final countResult = await _dataService.getEntriesCount(formName: "survey_form");
    print('Total entries: ${countResult.data}');

    // Get unexported count
    final unexportedResult = await _dataService.getUnexportedEntriesCount(
      formName: "survey_form",
    );
    print('Unexported: ${unexportedResult.data}');

#### Update Entry

    final result = await _dataService.updateEntry(
      formName: "survey_form",
      entryId: entry.id,
      data: {
        'name': 'Jane Doe', // Updated value
        'age': 26,
        // ... other updated fields
      },
    );

    if (result.wasSuccessful) {
      print('Entry updated successfully');
    }

#### Delete Entry

    final result = await _dataService.deleteEntry(
      formName: "survey_form",
      entry: entryToDelete,
    );

    result.fold(
      onSuccess: (_) => print('Entry deleted'),
      onFailure: (error) => print('Delete failed: ${error.message}'),
    );

### ☁️ Export to Cloud

    // Export data to cloud
    final result = await _dataService.exportToCloud(
      currentUser: currentUser,
      surveyId: survey.id,
      formId: form.id,
      onProgressUpdate: (ExportProgressModel progress) {
        print('Exporting batch ${progress.currentBatch}');
        print('Records exported: ${progress.exportedRecords}/${progress.totalRecords}');
        print('Status: ${progress.status}');
      },
    );

    result.fold(
      onSuccess: (exportResult) {
        print('Export completed!');
        print('Entries exported: ${exportResult.entriesExported}');
        print('Data export: ${exportResult.detailedResults.dataExportSuccess}');
        print('Summary export: ${exportResult.detailedResults.summaryExportSuccess}');

        // Check individual results
        final added = exportResult.detailedResults.addedEntriesResult;
        print('Added: ${added.successful}/${added.attempted}');

        final updated = exportResult.detailedResults.updatedEntriesResult;
        print('Updated: ${updated.successful}/${updated.attempted}');
      },
      onFailure: (error) {
        print('Export failed: ${error.message}');
      },
    );

### 🔍 Validation Workflow

    // Get validation fields (query parameters)
    final fieldsResult = await _dataService.getValidationFields(
      configId: survey.id,
    );

    // Get validation data from cloud
    final result = await _dataService.getValidationData(
      currentUser: currentUser,
      surveyId: survey.id,
      saveData: true, // Save to local database
      onProgressUpdate: (progress) {
        print('Fetching batch ${progress.currentBatch}');
        print('Fetched: ${progress.fetchedRecords}');
      },
    );

    result.fold(
      onSuccess: (finalProgress) {
        print('Validation data fetched!');
        print('Total records: ${finalProgress.totalRecords}');
      },
      onFailure: (error) => print('Fetch failed: ${error.message}'),
    );

### 📊 Status Codes Reference

Code | Status | Description | Export Behavior
--- | --- | --- | ---
0 | Draft | Entry not yet exported | Will be exported as new
1 | Exported | Successfully exported to cloud | Will be skipped
2 | Export Updated | Updated after initial export | Will be exported as update
3 | Exported (alt) | Alternative exported status | Can be re-exported
4 | Export Updated (alt) | Updated after export | Will be exported as update
5 | DL Unvalidated | Downloaded from cloud, not validated | Read-only
6 | DL Validated | Downloaded and validated | Can be exported
7 | DL Exported | Downloaded data, then exported | Will be skipped
8 | DL Revalidated | Downloaded, validated again | Will be exported

⚠️ Status Management

Status codes automatically change based on operations:

- New entries start at status 0 (Draft)
- After export, status changes to 1 (Exported)
- Updating an exported entry changes status to 2 (Export Updated)
- Downloaded entries have status 5-8

### 🖼️ Image Sync

    // Get unsynced images
    final result = await _dataService.getUnsyncedImages(formName: "survey_form");

    result.fold(
      onSuccess: (List images) {
        print('Unsynced images: ${images.length}');

        // Sync each image
        for (final image in images) {
          await _dataService.syncImage(
            imageId: image.imageId,
            imageUrl: image.imageUrl,
            token: currentUser.accessToken,
          );
        }
      },
      onFailure: (error) => print('Error: ${error.message}'),
    );

## Location Location Module

Hierarchical Nigerian location services with 6.8MB of CSV data covering zones, states, LGAs, wards, and polling units.

### 📍 Location Hierarchy

The module provides a 5-level location hierarchy:

1. Zone → Geopolitical zone (6 zones)
2. State → State/region (36 states + FCT)
3. LGA → Local Government Area (774 LGAs)
4. Ward → Electoral ward
5. Polling Unit → Polling station

### 🗃️ Location Service Types

Service | Data Source | Coverage
--- | --- | ---
InecLocationService | INEC (Electoral Commission) | Polling units, wards
NipostLocationService | NIPOST (Postal Service) | Localities, postal districts
MilsatLocationService | Milsat custom data | Custom location units

### 🔍 Basic Usage

    late final LocationService _locationService = LocationConfig.locationService;

    // Get all states
    final states = await _locationService.getAllStates();
    print('States: ${states.length}'); // 37 (36 + FCT)

    // Get LGAs for a state
    final lgas = await _locationService.getLgasByState('Lagos');
    print('LGAs in Lagos: ${lgas.length}'); // 20

    // Get wards for an LGA
    final wards = await _locationService.getWardsBy(
      state: 'Lagos',
      lga: 'Ikeja',
    );
    print('Wards in Ikeja: ${wards.length}');

    // Get polling units
    final pollingUnits = await _locationService.getPollingUnitsBy(
      state: 'Lagos',
      lga: 'Ikeja',
      ward: 'Ward 1',
    );
    print('Polling units: ${pollingUnits.length}');

💾 CSV Data Assets

The package includes 6.8MB of pre-loaded location data:

- `inec_pu.csv` - 6.1MB - INEC polling units
- `nipost_loc.csv` - 204KB - NIPOST localities
- `milsat.csv` - 470KB - Milsat custom locations
- `zones.csv` - 672 bytes - Geopolitical zones

## Map Map Module

Advanced Mapbox integration for interactive mapping, geometry drawing, and work area visualization.

### 🗺️ MapService Initialization

    final mapService = MapService(
      geometryType: MapConstants.polygon, // or .point, .line
      onMapReady: () {
        print('Map is ready');
      },
      onMapClicked: (lat, lng) {
        print('Map clicked at: $lat, $lng');
        return true; // Handle the click
      },
      onGeometryClicked: (geometryId) {
        print('Geometry $geometryId clicked');
      },
      onMapActionPerformed: (undoDepth, redoDepth) {
        print('Undo: $undoDepth, Redo: $redoDepth');
      },
    );

    // Initialize with Mapbox map instance
    await mapService.initialize(mapboxMap);

### ✏️ Drawing Operations

    // Start drawing mode
    await mapService.startDrawing();

    // Stop drawing mode
    await mapService.stopDrawing();

    // Clear current geometry
    await mapService.clearCurrentGeometry();

    // Undo last action
    await mapService.undoLastAction();

    // Redo last undone action
    await mapService.redoLastAction();

### 🎯 Geometry Management

    // Show a geometry on the map
    final geometry = GeometryEntity(
      id: 1,
      type: 'Polygon',
      coordinates: [/* coordinate arrays */],
    );
    await mapService.showGeometry(geometry);

    // Hide a geometry
    await mapService.hideGeometry(geometryId);

    // Clear all geometries
    await mapService.clearAllGeometries();

    // Get all shown geometries
    final geometries = mapService.getShownGeometries();

### 📐 Work Area Visualization

    // Show work area boundaries
    final workArea = WorkAreaModel(
      id: 'area_1',
      name: 'Survey Area 1',
      geometry: '{"type": "Polygon", "coordinates": [...]}',
    );
    await mapService.showWorkArea(workArea);

    // Hide work area
    await mapService.hideWorkArea('area_1');

    // Clear all work areas
    await mapService.clearAllWorkAreas();

### 📷 Camera Controls

    // Animate camera to location
    await mapService.animateCamera(
      latitude: 6.5244,
      longitude: 3.3792,
      zoom: 16.0,
    );

    // Zoom to specific feature
    await mapService.zoomToFeature(featureId);

    // Change base map style
    await mapService.setBaseMap(
      'mapbox://styles/mapbox/satellite-v9',
    );

🎨 MapConstants Reference

Commonly used map constants:

- `MapConstants.point` - "point"
- `MapConstants.line` - "line"
- `MapConstants.polygon` - "polygon"
- `MapConstants.defaultZoom` - 16.0
- `MapConstants.clickDebounceMs` - 300

## Best Practices & Gotchas

Critical tips, common mistakes, and recommended patterns for using milsat_modules_flutter.

### 🚨 Critical Setup Steps

❌ Common Mistakes to Avoid

1. Forgetting `WidgetsFlutterBinding.ensureInitialized()` before ModulesConfig
2. Not using `await` on ModulesConfig.initialize()
3. Initializing after `runApp()` instead of before
4. Accessing services before initialization
5. Not handling both `onSuccess` and `onFailure` in Result.fold()

### ✅ Recommended Patterns

#### 1. Service Injection

    // ✅ CORRECT - Use late final
    late final UserService _userService = UserConfig.userService;

    // ❌ WRONG - Don't initialize directly
    final UserService _userService = UserConfig.userService; // May fail!

#### 2. Error Handling

    // ✅ CORRECT - Always handle both cases
    result.fold(
      onSuccess: (data) { /* handle success */ },
      onFailure: (error) { /* handle error */ },
    );

    // ❌ WRONG - Don't ignore errors
    result.fold(
      onSuccess: (data) { /* ... */ },
      onFailure: (_) {}, // Empty handler!
    );

#### 3. Form Validation

    // ✅ CORRECT - Check if form data is valid
    final formData = await _navigationActions!.saveForm();
    if (formData != null) {
      // Form is valid, proceed
    } else {
      // Form has errors, show message
    }

    // ❌ WRONG - Don't assume saveForm always returns data
    final formData = await _navigationActions!.saveForm();
    await _dataService.createEntry(data: formData!); // May crash!

#### 4. Field Initialization

    // ✅ CORRECT - Initialize all fields before FormBuilder
    void _initializeAllFieldsInMap() {
      for (final page in survey.pages) {
        for (final field in page.fields) {
          _uiFieldValues[field.columnName] = null;
        }
      }
    }

    // ❌ WRONG - Don't use FormBuilder with empty uiFieldValues
    FormBuilder(
      uiFieldValues: {}, // Empty! Will cause issues
      // ...
    )

### ⚡ Performance Tips

- Use `late final` for service injection (lazy initialization)
- Initialize form fields once in `initState()`
- Avoid unnecessary `setState()` calls
- Use pagination when fetching large datasets
- Sync images separately before export to avoid timeouts

### 🔐 Security Considerations

- Tokens are automatically refreshed 60 seconds before expiry
- Tokens stored in secure storage (flutter_secure_storage)
- Always validate user input in forms
- Don't log sensitive data in production (use `isDebugMode: false`)

## API Reference & Appendix

Complete reference tables, dependencies, and troubleshooting resources.

### 📊 Module Comparison

Module | Purpose | Key Features | Main Dependencies
--- | --- | --- | ---
Common | Foundation | Error handling, Network, Config | dio, flutter_secure_storage
User | Auth & Users | Login, Profile, Projects | Inherits Common
Form | Dynamic Forms | FormBuilder, Widgets, Validation | image_picker, dropdown_textfield
Data | Data Management | CRUD, Export, Sync | sqflite, uuid
Location | Hierarchical Locations | States, LGAs, Wards, PUs | 6.8MB CSV data
Map | Mapping | Mapbox, Geometry, Drawing | mapbox_maps_flutter

### 📦 Complete Dependencies

    dependencies:
      # Core
      dio: ^5.9.0
      equatable: ^2.0.7
      get_it: ^8.2.0

      # Storage
      flutter_secure_storage: ^9.2.4
      shared_preferences: ^2.5.3
      sqflite: ^2.4.2
      path_provider: ^2.1.5

      # State Management
      flutter_bloc: ^9.1.1
      provider: ^6.1.5+1

      # UI & Forms
      image_picker: ^1.2.0
      file_picker: ^10.3.3
      dropdown_textfield: ^1.2.0
      flutter_svg: ^2.2.0

      # Utils
      intl: ^0.20.2
      uuid: ^4.5.1
      path: ^1.9.1
      expressions: ^0.2.5+2
      json_schema: ^5.2.1
      permission_handler: ^12.0.1
      device_info_plus: ^12.1.0

      # Mapping
      mapbox_maps_flutter: ^2.11.0

### ❓ Troubleshooting FAQ

#### Q: "Services are null or throwing errors"

A: Ensure you called `ModulesConfig.initialize()` with `await` before accessing services.

#### Q: "FormBuilder not showing fields"

A: Initialize `uiFieldValues` with all field column names before rendering FormBuilder.

#### Q: "Export fails with timeout"

A: Sync images separately first using `syncImage()`, then export data.

#### Q: "Location services return empty"

A: Ensure the CSV assets are included in your app's pubspec.yaml (they should be automatically included).

#### Q: "Token expired errors"

A: The package automatically refreshes tokens. If you see this error, the refresh failed. Check your refresh token configuration.

### 🔗 Useful Links

- GitHub Repository
- Issue Tracker
- Changelog

🎉 Ready to Build!

You now have everything you need to build sophisticated data collection applications with milsat_modules_flutter. Happy coding!