My App
Admin App DocsAiRefactoring

Task 3 Adapters Consolidation

SmartCrew Admin - Task 3 Adapters Consolidation

Task 3: Adapters Layer Consolidation and Optimization

Isolation Scope

This task is completely isolated and focuses ONLY on:

  • Optimizing adapter transformation logic for performance
  • Consolidating adapters with similar patterns
  • Creating base adapter classes for common transformations
  • NO changes to providers, repositories, services, or UI components

Dependencies: None (completely independent of Task 1 and Task 2) Affected Files: Only files in lib/util/adapter/ directory (17 adapter files) Not Included: Providers, repositories, services, or UI components

Goal

Reduce adapter code duplication by 60% and improve transformation performance by 25% while maintaining identical transformation results and public APIs.

Current State - Adapter Analysis

Adapter Categories (17 files total in lib/util/adapter/)

Simple Model-to-UI Adapters (12 files)

Current Files:

  • equipment_adapter.dart - Equipment to EquipmentRow transformation
  • labor_adapter.dart - AppUser to Labor transformation
  • material_orders_adapter.dart - Material orders to UI models
  • customer_projects_adapter.dart - Customer project transformations
  • document_adapter.dart - Document to UI document models
  • equipment_service_adapter.dart - Service record transformations
  • field_note_item_adapter.dart - Field notes to UI items
  • time_off_adapter.dart - Time off to UI models

Common Patterns Identified:

  • All follow same create() method pattern
  • Simple field mapping and data type conversion
  • Default value handling for null fields
  • Date formatting for UI display

Complex Report Adapters (5 files)

Current Files:

  • payroll_dailylog_adapter.dart - Complex payroll calculations
  • equipment_dot_reports_adapter.dart - DOT compliance reports
  • material_chart_adapter.dart - Chart data generation
  • equipment_hauling_adapter.dart - Hauling report data
  • admin_log_activity_adapter.dart - Admin activity reports

Performance Issues Identified:

  • Multiple iterations over same datasets
  • Inefficient date calculations
  • Redundant data transformations
  • No caching of expensive operations

Implementation Steps

Step 1: Create Base Adapter Infrastructure (Isolated)

1.1 Base Adapter Interface

File: lib/util/adapter/base_adapter.dart

abstract class BaseAdapter<TSource, TTarget> {
  /// Transforms a single source object to target type
  TTarget create(TSource source);
  
  /// Transforms a list of source objects to target type
  List<TTarget> createList(Iterable<TSource> sources) {
    return sources.map((source) => create(source)).toList();
  }
  
  /// Provides default value for null or invalid inputs
  TTarget createDefault();
  
  /// Validates source data before transformation
  bool isValidSource(TSource source);
}

abstract class CachedAdapter<TSource, TTarget> extends BaseAdapter<TSource, TTarget> {
  final Map<String, TTarget> _cache = {};
  
  /// Get cache key for source object (override for custom caching)
  String getCacheKey(TSource source);
  
  /// Cached transformation with automatic cache management
  TTarget createCached(TSource source) {
    if (!isValidSource(source)) return createDefault();
    
    final cacheKey = getCacheKey(source);
    return _cache.putIfAbsent(cacheKey, () => create(source));
  }
  
  /// Clear cache when needed
  void clearCache() => _cache.clear();
}

1.2 UI Adapter Base Class

File: lib/util/adapter/base_ui_adapter.dart

abstract class BaseUIAdapter<TDomain, TUI> extends CachedAdapter<TDomain, TUI> {
  
  /// Common date formatting for UI display
  String formatDate(DateTime? date) {
    return date?.toLocal().toString().split(' ')[0] ?? 'N/A';
  }
  
  /// Common time formatting for UI display
  String formatTime(DateTime? dateTime) {
    return dateTime != null 
        ? '${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}'
        : 'N/A';
  }
  
  /// Common duration formatting
  String formatDuration(Duration? duration) {
    if (duration == null) return '0:00';
    final hours = duration.inHours;
    final minutes = duration.inMinutes.remainder(60);
    return '${hours}:${minutes.toString().padLeft(2, '0')}';
  }
  
  /// Common null-safe string extraction
  String safeString(String? value, [String defaultValue = '']) {
    return value?.isNotEmpty == true ? value! : defaultValue;
  }
  
  /// Common number formatting
  String formatNumber(num? value, [int decimals = 2]) {
    return value?.toStringAsFixed(decimals) ?? '0.00';
  }
}

Step 2: Consolidate Similar Simple Adapters

2.1 Generic Model-to-UI Adapter

File: lib/util/adapter/model_to_ui_adapter.dart

enum UIAdapterType {
  equipment,
  labor,
  material,
  customer,
  document,
  timeOff,
}

class ModelToUIAdapterFactory {
  static BaseUIAdapter create(UIAdapterType type) {
    switch (type) {
      case UIAdapterType.equipment:
        return EquipmentUIAdapter();
      case UIAdapterType.labor:
        return LaborUIAdapter();
      case UIAdapterType.material:
        return MaterialUIAdapter();
      // ... other types
    }
  }
}

class EquipmentUIAdapter extends BaseUIAdapter<Equipment, EquipmentRow> {
  @override
  EquipmentRow create(Equipment source) {
    return EquipmentRow(
      equipment: source,
      name: safeString(source.name),
      category: safeString(source.type),
      year: source.year ?? DateTime.now().year,
      hours: source.hourRate.toInt(),
      lastService: source.lastServiceDate ?? DateTime.now(),
    );
  }
  
  @override
  String getCacheKey(Equipment source) => source.firebaseUid;
  
  @override
  bool isValidSource(Equipment source) => source.firebaseUid.isNotEmpty;
  
  @override
  EquipmentRow createDefault() => EquipmentRow.empty();
}

2.2 Consolidate Labor and User Adapters

Current Issues:

  • labor_adapter.dart creates Labor from AppUser with placeholder values
  • Similar pattern repeated in multiple adapters

Optimized Version:

class UserToLaborAdapter extends BaseUIAdapter<AppUser, Labor> {
  final bool includeTimeData;
  final DateTime? calculationDate;
  
  const UserToLaborAdapter({
    this.includeTimeData = false,
    this.calculationDate,
  });
  
  @override
  Labor create(AppUser source) {
    return Labor(
      firebaseUid: source.firebaseUid,
      phoneNumber: safeString(source.phoneNumber),
      employee: safeString(source.fullName),
      role: safeString(source.role),
      status: source.active,
      totalHours: includeTimeData ? _calculateTotalHours(source) : 'no total hours',
      shopHours: includeTimeData ? _calculateShopHours(source) : 'no shop hours',
      travelHours: includeTimeData ? _calculateTravelHours(source) : 'no travel hours',
      machineHours: includeTimeData ? _calculateMachineHours(source) : 'no machine hours',
    );
  }
  
  @override
  String getCacheKey(AppUser source) => 
      '${source.firebaseUid}_${includeTimeData}_${calculationDate?.millisecondsSinceEpoch}';
  
  // Optimized time calculations with caching
  String _calculateTotalHours(AppUser user) {
    if (calculationDate == null) return 'no total hours';
    
    // Use optimized service layer (from Task 2) if available
    // Otherwise maintain current calculation logic
    return _cachedTimeCalculation(user, 'total');
  }
  
  String _cachedTimeCalculation(AppUser user, String type) {
    final cacheKey = '${user.firebaseUid}_${type}_${calculationDate?.millisecondsSinceEpoch}';
    return _timeCache.putIfAbsent(cacheKey, () => _performCalculation(user, type));
  }
  
  static final Map<String, String> _timeCache = {};
}

Step 3: Optimize Complex Report Adapters

3.1 Optimize Payroll Report Adapter

Current Issues in payroll_dailylog_adapter.dart:

  • Multiple iterations over same data
  • Expensive date calculations repeated
  • No caching of intermediate results

Optimized Version:

class PayrollDailylogAdapter extends CachedAdapter<List<DailyLog>, PayrollReport> {
  final DateTime payrollDate;
  
  PayrollDailylogAdapter({required this.payrollDate});
  
  @override
  PayrollReport create(List<DailyLog> source) {
    if (source.isEmpty) return PayrollReport.empty();
    
    // Single-pass data processing instead of multiple iterations
    final processedData = _processDailyLogsEfficiently(source);
    
    return PayrollReport(
      totalHours: processedData.totalHours,
      regularHours: processedData.regularHours,
      overtimeHours: processedData.overtimeHours,
      employeeBreakdowns: processedData.employeeBreakdowns,
      periodStart: processedData.periodStart,
      periodEnd: processedData.periodEnd,
    );
  }
  
  // Optimized single-pass processing
  ProcessedPayrollData _processDailyLogsEfficiently(List<DailyLog> logs) {
    final Map<String, EmployeeHours> employeeHours = {};
    DateTime? earliestDate;
    DateTime? latestDate;
    
    for (final log in logs) {
      // Update date range
      earliestDate = earliestDate == null || log.logDate.isBefore(earliestDate) 
          ? log.logDate 
          : earliestDate;
      latestDate = latestDate == null || log.logDate.isAfter(latestDate) 
          ? log.logDate 
          : latestDate;
      
      // Process employee hours in single pass
      final employeeId = log.userId;
      final hours = employeeHours.putIfAbsent(employeeId, () => EmployeeHours.zero());
      
      // Calculate hours for this log
      final logHours = _calculateLogHours(log);
      hours.addHours(logHours);
    }
    
    return ProcessedPayrollData(
      employeeBreakdowns: employeeHours,
      periodStart: earliestDate ?? payrollDate,
      periodEnd: latestDate ?? payrollDate,
    );
  }
  
  @override
  String getCacheKey(List<DailyLog> source) {
    // Create cache key from log IDs and payroll date
    final logIds = source.map((log) => log.id).join(',');
    return '${logIds}_${payrollDate.millisecondsSinceEpoch}'.hashCode.toString();
  }
  
  @override
  bool isValidSource(List<DailyLog> source) => source.isNotEmpty;
  
  @override
  PayrollReport createDefault() => PayrollReport.empty();
}

3.2 Optimize Material Chart Adapter

Current Issues: Inefficient data aggregation and chart point generation

Optimized Version:

class MaterialChartAdapter extends CachedAdapter<List<MaterialOrder>, MaterialChartData> {
  final DateRange dateRange;
  final ChartType chartType;
  
  @override
  MaterialChartData create(List<MaterialOrder> source) {
    if (source.isEmpty) return MaterialChartData.empty();
    
    // Filter data once, then reuse for multiple chart calculations
    final filteredOrders = _filterOrdersByDateRange(source);
    
    return MaterialChartData(
      dataPoints: _generateChartPoints(filteredOrders),
      totalValue: _calculateTotalValue(filteredOrders),
      averageOrderValue: _calculateAverageValue(filteredOrders),
      peakOrderDate: _findPeakOrderDate(filteredOrders),
    );
  }
  
  // Optimized chart point generation
  List<ChartPoint> _generateChartPoints(List<MaterialOrder> orders) {
    final Map<DateTime, double> dailyTotals = {};
    
    // Single pass to aggregate daily totals
    for (final order in orders) {
      final date = order.orderDate.dateOnly;
      dailyTotals[date] = (dailyTotals[date] ?? 0.0) + order.totalValue;
    }
    
    // Convert to chart points efficiently
    return dailyTotals.entries
        .map((entry) => ChartPoint(date: entry.key, value: entry.value))
        .toList()
      ..sort((a, b) => a.date.compareTo(b.date));
  }
  
  @override
  String getCacheKey(List<MaterialOrder> source) {
    return '${chartType.name}_${dateRange.hashCode}_${source.length}_${source.first.id}';
  }
}

Step 4: Create Adapter Factory Pattern

4.1 Universal Adapter Factory

File: lib/util/adapter/adapter_factory.dart

class AdapterFactory {
  // Singleton pattern for performance
  static final Map<String, BaseAdapter> _adapters = {};
  
  static T getAdapter<T extends BaseAdapter>(
    AdapterType type, {
    Map<String, dynamic>? config,
  }) {
    final key = '${type.name}_${config?.hashCode ?? 0}';
    
    return _adapters.putIfAbsent(key, () {
      switch (type) {
        case AdapterType.equipmentToUI:
          return EquipmentUIAdapter();
        case AdapterType.userToLabor:
          return UserToLaborAdapter(
            includeTimeData: config?['includeTimeData'] ?? false,
            calculationDate: config?['calculationDate'],
          );
        case AdapterType.payrollReport:
          return PayrollDailylogAdapter(
            payrollDate: config!['payrollDate'],
          );
        // ... other adapters
      }
    }) as T;
  }
  
  // Bulk transformation for performance
  static List<TTarget> transformList<TSource, TTarget>(
    List<TSource> sources,
    AdapterType adapterType, {
    Map<String, dynamic>? config,
  }) {
    final adapter = getAdapter<BaseAdapter<TSource, TTarget>>(adapterType, config: config);
    return adapter.createList(sources);
  }
  
  // Clear all cached adapters
  static void clearAdapterCache() {
    for (final adapter in _adapters.values) {
      if (adapter is CachedAdapter) {
        adapter.clearCache();
      }
    }
  }
}

enum AdapterType {
  equipmentToUI,
  userToLabor,
  materialChart,
  payrollReport,
  documentToUI,
  timeOffToUI,
  // ... other adapter types
}

Testing & Validation Metrics

Pre-Optimization Baseline Tests

# 1. Capture Current Transformation Results
void captureTransformationBaseline() {
  // Equipment adapter baseline
  final equipment = createTestEquipment();
  final equipmentRow = EquipmentAdapter(equipments: [equipment]).create();
  storeBaseline('equipment_transform', equipmentRow);
  
  // Labor adapter baseline
  final user = createTestUser();
  final labor = LaborAdapter(user: user).create();
  storeBaseline('labor_transform', labor);
  
  // Complex report baseline
  final logs = createTestDailyLogs();
  final payrollReport = PayrollDailylogAdapter().create(logs);
  storeBaseline('payroll_transform', payrollReport);
}

# 2. Performance Baseline
void captureAdapterPerformance() {
  final stopwatch = Stopwatch()..start();
  
  // Simple adapter performance
  final equipmentTime = measureEquipmentAdapterPerformance();
  print('Equipment adapter baseline: ${equipmentTime}ms');
  
  // Complex adapter performance
  final payrollTime = measurePayrollAdapterPerformance();
  print('Payroll adapter baseline: ${payrollTime}ms');
  
  // Bulk transformation performance
  final bulkTime = measureBulkTransformationPerformance();
  print('Bulk transformation baseline: ${bulkTime}ms');
}

# 3. Memory Usage Baseline
void captureAdapterMemoryBaseline() {
  final memoryBefore = getMemoryUsage();
  runAllAdapterTransformations();
  final memoryAfter = getMemoryUsage();
  final baseline = memoryAfter - memoryBefore;
  print('Adapter memory baseline: ${baseline}MB');
}

Post-Optimization Validation Metrics

Metric 1: Identical Transformation Results

# All transformations must return EXACTLY the same results
void testTransformationAccuracy() {
  final beforeResults = runBaselineTransformations();
  final afterResults = runOptimizedTransformations();
  
  // Equipment transformation accuracy
  expect(afterResults.equipmentRow.name, equals(beforeResults.equipmentRow.name));
  expect(afterResults.equipmentRow.hours, equals(beforeResults.equipmentRow.hours));
  expect(afterResults.equipmentRow.category, equals(beforeResults.equipmentRow.category));
  
  // Labor transformation accuracy
  expect(afterResults.labor.employee, equals(beforeResults.labor.employee));
  expect(afterResults.labor.totalHours, equals(beforeResults.labor.totalHours));
  
  // Complex report accuracy
  expect(afterResults.payrollReport.totalHours, equals(beforeResults.payrollReport.totalHours));
  expect(afterResults.payrollReport.overtimeHours, equals(beforeResults.payrollReport.overtimeHours));
  
  // Test with edge cases
  testTransformationWithNullData();
  testTransformationWithEmptyData();
  testTransformationWithLargeDatasets();
}

Metric 2: Performance Improvement (25% target)

void testAdapterPerformanceImprovement() {
  // Simple adapters should be 25% faster
  final equipmentTime = measureEquipmentAdapterTime();
  expect(equipmentTime, lessThan(baselineEquipmentTime * 0.75));
  
  // Complex adapters should be 30% faster
  final payrollTime = measurePayrollAdapterTime();
  expect(payrollTime, lessThan(baselinePayrollTime * 0.70));
  
  // Bulk transformations should be 40% faster
  final bulkTime = measureBulkTransformationTime();
  expect(bulkTime, lessThan(baselineBulkTime * 0.60));
  
  // Cache performance - second call should be 90% faster
  final firstCall = measureAdapterCall();
  final secondCall = measureAdapterCall(); // Should hit cache
  expect(secondCall, lessThan(firstCall * 0.1));
}

Metric 3: Code Duplication Reduction (60% target)

void testCodeDuplicationReduction() {
  final beforeDuplicateLines = countAdapterDuplicateLines();
  final afterDuplicateLines = countAdapterDuplicateLines();
  
  final reduction = (beforeDuplicateLines - afterDuplicateLines) / beforeDuplicateLines;
  expect(reduction, greaterThan(0.6)); // 60% reduction target
  
  // Verify base classes are being used
  final equipmentAdapter = AdapterFactory.getAdapter(AdapterType.equipmentToUI);
  expect(equipmentAdapter, isA<BaseUIAdapter>());
  
  // Verify factory pattern consolidation
  expect(AdapterFactory._adapters.length, greaterThan(0));
}

Metric 4: Memory Usage Reduction (20% target)

void testAdapterMemoryOptimization() {
  final memoryUsage = measureAdapterMemoryUsage();
  expect(memoryUsage, lessThan(baselineAdapterMemoryUsage * 0.8));
  
  // Test memory efficiency with large datasets
  final largeDatasetMemory = measureLargeDatasetTransformation();
  expect(largeDatasetMemory, lessThan(acceptableMemoryThreshold));
  
  // Cache memory management
  AdapterFactory.clearAdapterCache();
  final memoryAfterClear = getMemoryUsage();
  expect(memoryAfterClear, lessThan(memoryBeforeClear * 1.1));
}

Metric 5: Adapter API Compatibility

void testAdapterAPICompatibility() {
  // All existing adapter constructors must work
  expect(() => EquipmentAdapter(equipments: [equipment]), returnsNormally);
  expect(() => LaborAdapter(user: user), returnsNormally);
  expect(() => PayrollDailylogAdapter(), returnsNormally);
  
  // All create() methods must work unchanged
  final equipmentResult = EquipmentAdapter(equipments: [equipment]).create();
  expect(equipmentResult, isA<List<EquipmentRow>>());
  
  final laborResult = LaborAdapter(user: user).create();
  expect(laborResult, isA<Labor>());
  
  // Factory methods should provide same results
  final factoryResult = AdapterFactory.getAdapter(AdapterType.equipmentToUI).create(equipment);
  final directResult = EquipmentAdapter(equipments: [equipment]).create().first;
  expect(factoryResult.name, equals(directResult.name));
}

Metric 6: Edge Case and Error Handling

void testAdapterEdgeCaseHandling() {
  // Null input handling
  final nullResult = adapter.create(null);
  expect(nullResult, equals(adapter.createDefault()));
  
  // Empty list handling
  final emptyResult = adapter.createList([]);
  expect(emptyResult, isEmpty);
  
  // Invalid data handling
  final invalidResult = adapter.create(invalidData);
  expect(invalidResult, equals(baseline_InvalidResult));
  
  // Large dataset handling
  final largeDatasetTime = measureLargeDatasetAdapterTime();
  expect(largeDatasetTime, lessThan(acceptableMaxTime));
}

Success Validation Checklist

Performance Validation

  • 25% improvement in simple adapter performance
  • 30% improvement in complex adapter performance
  • 40% improvement in bulk transformation performance
  • Cache hit rate > 85% for repeated transformations
  • Memory usage reduced by 20%

Accuracy Validation

  • All transformations return identical results to baseline
  • Edge cases handled identically
  • Null and empty data handled correctly
  • Data type conversions maintain precision
  • Date/time formatting identical

Code Quality Validation

  • 60% reduction in duplicate code lines
  • All adapters use appropriate base classes
  • Factory pattern implemented correctly
  • Proper error handling maintained
  • Documentation updated for new patterns

Compatibility Validation

  • All existing adapter APIs unchanged
  • Constructor signatures preserved
  • Return types identical
  • Exception handling behavior preserved
  • Thread safety maintained

Rollback Strategy

Immediate Rollback

  • Keep original adapter files as .backup during development
  • Restore individual adapters if any metric fails
  • Git branch strategy allows atomic rollbacks per adapter

Selective Rollback

  • Rollback can be done per adapter type:
    • Simple UI adapters independently
    • Complex report adapters independently
    • Factory pattern can be disabled

Validation Gates

  • Each adapter optimization must pass all metrics before proceeding
  • Automated testing prevents broken transformations
  • Performance regression detection stops deployment

Files Modified (Complete List)

New Base Adapter Files

  • lib/util/adapter/base_adapter.dart
  • lib/util/adapter/base_ui_adapter.dart
  • lib/util/adapter/adapter_factory.dart

New Consolidated Adapters

  • lib/util/adapter/model_to_ui_adapter.dart
  • lib/util/adapter/report_adapter.dart

Modified Simple Adapters (8 files optimized)

  • lib/util/adapter/equipment_adapter.dart (uses base class)
  • lib/util/adapter/labor_adapter.dart (uses base class)
  • lib/util/adapter/material_orders_adapter.dart (consolidated)
  • lib/util/adapter/customer_projects_adapter.dart (uses base class)
  • lib/util/adapter/document_adapter.dart (uses base class)
  • lib/util/adapter/time_off_adapter.dart (uses base class)
  • lib/util/adapter/field_note_item_adapter.dart (uses base class)
  • lib/util/adapter/equipment_service_adapter.dart (uses base class)

Modified Complex Adapters (5 files optimized)

  • lib/util/adapter/payroll_dailylog_adapter.dart (performance optimized)
  • lib/util/adapter/equipment_dot_reports_adapter.dart (optimized)
  • lib/util/adapter/material_chart_adapter.dart (optimized)
  • lib/util/adapter/equipment_hauling_adapter.dart (optimized)
  • lib/util/adapter/admin_log_activity_adapter.dart (optimized)

Deprecated Adapters (4 files)

  • Simple adapters replaced by factory pattern
  • Kept for backward compatibility during transition

Total Files: 20 (5 new, 13 modified, 2 consolidated) Scope: Only adapter files - no providers, repositories, services, or UI touched

On this page