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
TimePickerFieldwith 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
reportscollection - Firebase Storage: For image uploads via
DigitalOceanProvider - Authentication: User-scoped reports using
userIdfield - Workspace: Multi-tenant support using
workspacefield
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
- Offline Support: Store reports locally when offline
- Bulk Operations: Upload multiple materials at once
- Material Templates: Predefined material lists
- Cost Tracking: Integration with budget systems
- Notifications: Alerts for low material quantities
- Reporting: Analytics dashboard for material usage
API Extensions
- Material Catalogs: Integration with supplier APIs
- Inventory Management: Real-time stock tracking
- Purchase Orders: Direct ordering from the app
- 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.