My App

Materials

SmartCrew Admin - Materials

Material Field Usage & Material Shopping Features Documentation

Overview

This document provides comprehensive documentation for the Material Field Usage and Material Shopping report features implemented in the SmartCrew Mobile application. These features allow users to track material usage on job sites and material purchases for projects respectively.

Feature Summary

Material Field Usage Report

  • Purpose: Track material usage on job sites with date, project, and material details
  • DocType: materialFieldUsage
  • Key Fields: Date, Project, Material, Images
  • Navigation: Available in Reports screen

Material Shopping Report

  • Purpose: Track material purchases for projects with vendor and cost information
  • DocType: materialShopping
  • Key Fields: Project, Item, Vendor, Quantity, Cost, Notes, Photos
  • Navigation: Available in Reports screen

Implementation Details

1. Document Type Enum Updates

File: lib/models/document.dart

Added two new document types to the DocType enum:

enum DocType {
  incident,
  service,
  addService,
  hauling,
  materialFieldUsage,    // Added for material field usage tracking
  materialShopping,      // Added for material shopping tracking
  inspect,
  offTime,
  documents
}

2. Form Data Models

Material Field Usage Form Data

File: lib/models/form_data/material_field_usage_form_data.dart

class MaterialFieldUsageFormData with FormDataMixin {
  final DateTime? date;
  final String? projectId;
  final String? material;

  MaterialFieldUsageFormData({
    this.date,
    this.projectId,
    this.material,
  });

  @override
  Map<String, dynamic> toJson() {
    return {
      'date': date?.millisecondsSinceEpoch,
      'projectId': projectId,
      'material': material,
    };
  }

  MaterialFieldUsageFormData copyWith({
    DateTime? date,
    String? projectId,
    String? material,
  }) {
    return MaterialFieldUsageFormData(
      date: date ?? this.date,
      projectId: projectId ?? this.projectId,
      material: material ?? this.material,
    );
  }
}

Material Shopping Form Data

File: lib/models/form_data/material_shopping_form_data.dart

class MaterialShoppingFormData with FormDataMixin {
  final String? projectId;
  final String? item;
  final String? vendor;
  final String? vendorId;
  final String? quantity;
  final String? cost;
  final String? notes;
  final List<String>? photos;

  MaterialShoppingFormData({
    this.projectId,
    this.item,
    this.vendor,
    this.vendorId,
    this.quantity,
    this.cost,
    this.notes,
    this.photos,
  });

  @override
  Map<String, dynamic> toJson() {
    return {
      'projectId': projectId,
      'project': projectId, // For compatibility
      'item': item,
      'vendor': vendorId, // Use vendorId for storage, not vendor name
      'quantity': quantity,
      'cost': cost,
      'notes': notes,
      'photos': photos,
      'materialShopping': true, // Flag to identify this as material shopping
    };
  }

  MaterialShoppingFormData copyWith({
    String? projectId,
    String? item,
    String? vendor,
    String? vendorId,
    String? quantity,
    String? cost,
    String? notes,
    List<String>? photos,
    bool clearVendor = false,
    bool clearVendorId = false,
  }) {
    return MaterialShoppingFormData(
      projectId: projectId ?? this.projectId,
      item: item ?? this.item,
      vendor: clearVendor ? null : (vendor ?? this.vendor),
      vendorId: clearVendorId ? null : (vendorId ?? this.vendorId),
      quantity: quantity ?? this.quantity,
      cost: cost ?? this.cost,
      notes: notes ?? this.notes,
      photos: photos ?? this.photos,
    );
  }
}

3. Report Models

File: lib/models/report.dart

Added corresponding report models for data retrieval:

class MaterialFieldUsageReport extends BaseReport {
  final DateTime date;
  final String projectId;
  final String material;
  final List<String>? images;

  MaterialFieldUsageReport({
    required this.date,
    required this.projectId,
    required this.material,
    this.images,
  });

  factory MaterialFieldUsageReport.fromJson(Map<String, dynamic> map) {
    return MaterialFieldUsageReport(
      date: DateTime.fromMillisecondsSinceEpoch(map['date']),
      projectId: map['projectId'] ?? map['project'] ?? '',
      material: map['material'] ?? '',
      images: map['images'] == null ? null : List<String>.from(map['images']),
    );
  }
}

class MaterialShoppingReport extends BaseReport {
  final String projectId;
  final String item;
  final String? vendor;
  final String? quantity;
  final String? cost;
  final String? notes;
  final List<String>? photos;

  MaterialShoppingReport({
    required this.projectId,
    required this.item,
    this.vendor,
    this.quantity,
    this.cost,
    this.notes,
    this.photos,
  });

  factory MaterialShoppingReport.fromJson(Map<String, dynamic> map) {
    return MaterialShoppingReport(
      projectId: map['projectId'] ?? map['project'] ?? '',
      item: map['item'] ?? '',
      vendor: map['vendor'],
      quantity: map['quantity'],
      cost: map['cost'],
      notes: map['notes'],
      photos: map['photos'] == null ? null : List<String>.from(map['photos']),
    );
  }
}

4. Data Validation

Material Field Usage Validation

File: lib/screens/logged_in/home_tabs/docs/validation/material_field_usage_data_validation.dart

class MaterialFieldUsageDataValidation extends BaseFormDataValidation<MaterialFieldUsageFormData> {
  MaterialFieldUsageDataValidation({
    DateTime? date,
    String? projectId,
    String? material,
    List<String>? images,
  }) : super(
          data: MaterialFieldUsageFormData(
            date: date ?? DateTime.now(),
            projectId: projectId,
            material: material,
          ),
        );

  @override
  bool isValid() {
    return data.date != null &&
           data.projectId?.isNotEmpty == true &&
           data.material?.isNotEmpty == true;
  }
}

Material Shopping Validation

File: lib/screens/logged_in/home_tabs/docs/validation/material_shopping_data_validation.dart

class MaterialShoppingDataValidation extends BaseFormDataValidation<MaterialShoppingFormData> {
  MaterialShoppingDataValidation({
    String? projectId,
    String? item,
    String? vendor,
    String? vendorId,
    String? quantity,
    String? cost,
    String? notes,
    List<String>? photos,
  }) : super(
          data: MaterialShoppingFormData(
            projectId: projectId,
            item: item,
            vendor: vendor,
            vendorId: vendorId,
            quantity: quantity,
            cost: cost,
            notes: notes,
            photos: photos,
          ),
        );

  @override
  bool isValid() {
    // Required fields: projectId, item, quantity
    return data.projectId?.isNotEmpty == true &&
           data.item?.isNotEmpty == true &&
           data.quantity?.isNotEmpty == true;
  }
}

5. Report Screens

Material Field Usage Report Screen

File: lib/screens/logged_in/home_tabs/docs/material_field_usage_report_screen.dart

Key features:

  • Date Picker: Uses TimePickerField with default current date
  • Project Dropdown: Uses ProjectLabelDropdown (required field)
  • Material Input: Text input field (required field)
  • Form Integration: Extends BaseReportScreen<MaterialFieldUsageFormData>

UI Components:

// Date picker with current date default
TimePickerField(
  onDateChanged: (value) {
    widget.mutableContainer.update(
      widget.mutableContainer.value.copyWith(date: value),
    );
  },
  controller: _dateController,
  hint: DateTime.now(),
)

// Project dropdown
ProjectLabelDropdown(
  label: 'Project *',
  data: dataProvider.projects,
  selectedProjectId: _selectedProjectId,
  onChanged: (projectId) {
    setState(() => _selectedProjectId = projectId);
    widget.mutableContainer.update(
      widget.mutableContainer.value.copyWith(projectId: projectId),
    );
  },
)

// Material input
InputField(
  controller: _materialController,
  hintText: 'Enter material name',
  input: TextInputType.text,
)

Material Shopping Report Screen

File: lib/screens/logged_in/home_tabs/docs/material_shopping_report_screen.dart

Key features:

  • Project Dropdown: Required field
  • Item Input: Required text field
  • Vendor Dropdown: Optional vendor selection
  • Quantity Input: Required numeric field
  • Cost Input: Optional numeric field
  • Notes Input: Optional text area
  • Photo Upload: Uses MultiImageUploadComponent

UI Components:

// Vendor dropdown with optional selection
VendorLabelDropdown(
  label: 'Vendor (Optional)',
  data: dataProvider.vendors,
  selectedVendorId: _selectedVendorId,
  onChanged: (vendorId) {
    setState(() => _selectedVendorId = vendorId);
    if (vendorId != null) {
      final vendor = dataProvider.vendors.firstWhere((v) => v.id == vendorId);
      widget.mutableContainer.update(
        widget.mutableContainer.value.copyWith(
          vendorId: vendorId,
          vendor: vendor.companyName,
        ),
      );
    } else {
      widget.mutableContainer.update(
        widget.mutableContainer.value.copyWith(
          clearVendor: true,
          clearVendorId: true,
        ),
      );
    }
  },
)

// Photo upload component
MultiImageUploadComponent(
  initialImages: widget.mutableContainer.value.photos,
  valueChanged: (images) {
    widget.mutableContainer.update(
      widget.mutableContainer.value.copyWith(photos: images),
    );
  },
)

6. Report History Integration

Report History Factory

File: lib/providers/network_data_provider/report/report_history_factory.dart

Added cases for both report types:

class ReportHistoryFactory {
  ReportHistory create(Map<String, dynamic> doc, String id) {
    final DocType type = DocType.values.byName(doc['type']);
    return switch (type) {
      // ... existing cases
      DocType.materialShopping => ReportHistory.materialShopping(doc, id, MaterialShoppingReport.fromJson),
      DocType.materialFieldUsage => ReportHistory.materialFieldUsage(doc, id, MaterialFieldUsageReport.fromJson),
      // ... remaining cases
    };
  }
}

Report History Detail Factory

File: lib/screens/logged_in/home_tabs/docs/history/report_history_detail_factory.dart

Added detail view builders for both report types:

class ReportHistoryDetailFactory {
  List<Widget> create(BaseReport type, BuildContext context) {
    switch (type.runtimeType) {
      // ... existing cases
      case MaterialShoppingReport:
        return _buildMaterialShoppingReportDetail(type as MaterialShoppingReport, context);
      case MaterialFieldUsageReport:
        return _buildMaterialFieldUsageReportDetail(type as MaterialFieldUsageReport, context);
      // ... remaining cases
    }
    return [];
  }

  // Material Shopping detail view
  List<Widget> _buildMaterialShoppingReportDetail(MaterialShoppingReport materialShoppingReport, BuildContext context) {
    final String projectName = NetworkDataProvider().getByProjectId(materialShoppingReport.projectId)?.projectName ?? 'Unknown Project';

    return [
      ReportDetailItemComponent(title: 'Project', value: projectName),
      ReportDetailItemComponent(title: 'Item', value: materialShoppingReport.item),
      if (materialShoppingReport.vendor?.isNotEmpty == true)
        ReportDetailItemComponent(title: 'Vendor', value: materialShoppingReport.vendor),
      if (materialShoppingReport.quantity?.isNotEmpty == true)
        ReportDetailItemComponent(title: 'Quantity', value: materialShoppingReport.quantity),
      if (materialShoppingReport.cost?.isNotEmpty == true)
        ReportDetailItemComponent(title: 'Cost', value: materialShoppingReport.cost),
      if (materialShoppingReport.notes.isNotNullAndEmpty)
        ReportDetailItemComponent(title: 'Notes', value: materialShoppingReport.notes, isSingleLine: false),
      if (materialShoppingReport.photos?.isNotEmpty == true)
        Wrap(
          spacing: 12,
          runSpacing: 12,
          children: [
            ...materialShoppingReport.photos!.map((entry) {
              return _buildImageContainer(
                child: _buildImageTile(entry, context),
              );
            }),
          ],
        ),
    ];
  }

  // Material Field Usage detail view
  List<Widget> _buildMaterialFieldUsageReportDetail(MaterialFieldUsageReport materialFieldUsageReport, BuildContext context) {
    final String projectName = NetworkDataProvider().getByProjectId(materialFieldUsageReport.projectId)?.projectName ?? 'Unknown Project';

    return [
      ReportDetailItemComponent(title: 'Date', value: materialFieldUsageReport.date.formatCustomDate()),
      ReportDetailItemComponent(title: 'Project', value: projectName),
      ReportDetailItemComponent(title: 'Material', value: materialFieldUsageReport.material),
      if (materialFieldUsageReport.images?.isNotEmpty == true)
        Wrap(
          spacing: 12,
          runSpacing: 12,
          children: [
            ...materialFieldUsageReport.images!.map((entry) {
              return _buildImageContainer(
                child: _buildImageTile(entry, context),
              );
            }),
          ],
        ),
    ];
  }
}

7. Navigation Integration

Reports Screen Integration

File: lib/screens/logged_in/home_tabs/reports_screen.dart

Added imports and navigation cases:

// Added import
import 'docs/material_field_usage_report_screen.dart';

// Added navigation case for Material Field Usage
case DocType.materialFieldUsage:
  _openReport(
    context,
    MaterialFieldUsageReportScreen(
      onHistoryClick: () => _showHistoryModal(
        context,
        value[index].type,
        'Material Field Usage Report',
        value[index].icon,
      ),
      title: 'Material Field Usage Report',
      icon: _buildIcon(
        Center(child: value[index].icon),
      ),
    ),
  );

// Material Shopping navigation was already implemented
case DocType.materialShopping:
  _openReport(
    context,
    MaterialShoppingReportScreen(
      onHistoryClick: () => _showHistoryModal(
        context,
        value[index].type,
        'Material Shopping Report',
        value[index].icon,
      ),
      title: 'Material Shopping Report',
      icon: _buildIcon(
        Center(child: value[index].icon),
      ),
    ),
  );

Report Provider Integration

File: lib/providers/network_data_provider/report/report_provider.dart

Added both reports to the reportDocs list:

final List<ReportTypeDisplay> reportDocs = [
  // ... existing reports
  ReportTypeDisplay(
    title: 'Material Shopping Report',
    content: 'Track material purchases for projects',
    type: DocType.materialShopping,
    icon: Assets.images.document.inventory.svg(height: 20, width: 20),
    payload: null,
  ),
  ReportTypeDisplay(
    title: 'Material Field Usage Report',
    content: 'Track material usage on job sites',
    type: DocType.materialFieldUsage,
    icon: Assets.images.document.inventory.svg(height: 20, width: 20),
    payload: null,
  ),
  // ... remaining reports
];

8. Bug Fixes Applied

MultiImageUploadComponent Fix

File: lib/components/custom/multi_image_upload_component.dart

Fixed an issue where the component wasn't responding to changes in the initialImages prop:

@override
void didUpdateWidget(MultiImageUploadComponent oldWidget) {
  super.didUpdateWidget(oldWidget);
  if (widget.initialImages != oldWidget.initialImages) {
    _imageUrls = widget.initialImages ?? [];
  }
}

This ensures that when form data is updated and the initialImages list changes, the component reflects those changes in its internal state.

API Integration

Data Storage Format

Material Field Usage

{
  "type": "materialFieldUsage",
  "date": 1640995200000,
  "projectId": "project_id_here",
  "material": "Concrete",
  "creationTime": 1640995200000,
  "workspace": "workspace_id",
  "userId": "user_id"
}

Material Shopping

{
  "type": "materialShopping",
  "projectId": "project_id_here",
  "project": "project_id_here",
  "item": "Steel Rebar",
  "vendor": "vendor_id_here",
  "quantity": "100",
  "cost": "5000",
  "notes": "High quality steel for foundation",
  "photos": ["url1", "url2"],
  "materialShopping": true,
  "creationTime": 1640995200000,
  "workspace": "workspace_id",
  "userId": "user_id"
}

Firebase Integration

Both features integrate with:

  • Firestore: For data storage in the reports collection
  • Firebase Storage: For image uploads via DigitalOceanProvider
  • Authentication: User-scoped reports using userId field
  • Workspace: Multi-tenant support using workspace field

UI/UX Guidelines

Form Validation

  • Material Field Usage: Date, Project, and Material are required
  • Material Shopping: Project, Item, and Quantity are required
  • Real-time validation feedback using BaseFormDataValidation

User Experience

  • Both reports follow the same UI patterns as existing reports
  • Consistent use of SmartCrew design components
  • Photo upload with progress indicators and error handling
  • Proper date picker with current date default
  • Dropdown components with search and selection capabilities

Accessibility

  • Proper labeling of form fields
  • Required field indicators (*)
  • Error message display
  • Touch-friendly button sizes

Testing Considerations

Form Validation Testing

  • Test required field validation
  • Test form submission with valid/invalid data
  • Test photo upload functionality
  • Test project and vendor dropdown selections

Integration Testing

  • Test report submission to Firebase
  • Test report history retrieval and display
  • Test report detail view rendering
  • Test navigation between screens

Edge Cases

  • Empty project/vendor lists
  • Network failures during image upload
  • Large image file handling
  • Form state persistence during navigation

Future Enhancements

Potential Improvements

  1. Offline Support: Store reports locally when offline
  2. Bulk Operations: Upload multiple materials at once
  3. Material Templates: Predefined material lists
  4. Cost Tracking: Integration with budget systems
  5. Notifications: Alerts for low material quantities
  6. Reporting: Analytics dashboard for material usage

API Extensions

  1. Material Catalogs: Integration with supplier APIs
  2. Inventory Management: Real-time stock tracking
  3. Purchase Orders: Direct ordering from the app
  4. Approval Workflows: Multi-level approval for purchases

Conclusion

The Material Field Usage and Material Shopping features provide comprehensive material tracking capabilities for the SmartCrew Mobile application. Both features follow established patterns in the codebase and integrate seamlessly with existing report infrastructure.

The implementation includes proper form validation, photo upload capabilities, Firebase integration, and consistent UI/UX design. The modular architecture allows for easy maintenance and future enhancements.

On this page