Checkout
SmartCrew Admin - Checkout
Checkout Questions Feature Documentation
Overview
The Checkout Questions feature allows administrators to create custom questions that employees must answer when clocking out from work. This feature provides valuable insights into daily operations, safety concerns, equipment status, and other important aspects of work completion.
Feature Architecture
Admin Side (Web Console)
The admin console provides full CRUD operations for managing checkout questions through a dedicated settings tab.
Key Components
- Settings Tab: "Checkout Questions" tab in the admin settings
- Question Management: Create, edit, delete, and view all checkout questions
- Question Configuration:
- Question text (multiline support)
- Default answer (Yes/No)
- Document type categorization
Mobile Side Integration
When an employee attempts to clock out, the mobile app should:
- Fetch Questions: Retrieve all relevant checkout questions from Firebase
- Display Questions: Present questions in a user-friendly interface
- Collect Answers: Allow users to answer each question
- Submit Responses: Save responses to Firebase for admin review
- Complete Checkout: Only allow clock-out after all questions are answered
Data Structure
CheckoutQuestion Model
class CheckoutQuestion {
final String? id;
final String question;
final bool defaultAnswer;
final DocType docType;
final DateTime? createdAt;
final DateTime? updatedAt;
final String? companyId;
}Document Types (DocType Enum)
The system supports the following document types for categorizing questions:
incident- Incident Reportservice- Service RequestaddService- Add Service Requesthauling- Hauling LogmaterialFieldUsage- Material Field UsagematerialShopping- Material Shoppinginspect- Inspection ReportoffTime- Time Offdocuments- Document
CheckoutResponse Model (Recommended for Mobile)
class CheckoutResponse {
final String? id;
final String userId;
final String questionId;
final String question;
final bool response;
final DocType docType;
final DateTime timestamp;
final String? companyId;
final String? projectId;
final String? location;
}Firebase Integration
Firestore Collections
1. Checkout Questions Collection: checkout_questions
Document Structure:
{
"id": "auto-generated-id",
"question": "Did you encounter any safety issues during your shift?",
"defaultAnswer": false,
"docType": "incident",
"createdAt": 1703123456789,
"updatedAt": 1703123456789,
"companyId": "company-workspace-id"
}2. Checkout Responses Collection: checkout_responses (Mobile Implementation)
Document Structure:
{
"id": "auto-generated-id",
"userId": "employee-user-id",
"questionId": "question-document-id",
"question": "Did you encounter any safety issues during your shift?",
"response": true,
"docType": "incident",
"timestamp": 1703123456789,
"companyId": "company-workspace-id",
"projectId": "current-project-id",
"location": "job-site-location"
}Security Rules
Firestore Security Rules Example:
// Checkout Questions - Admin only
match /checkout_questions/{questionId} {
allow read: if isAuthenticated();
allow write: if isAdmin();
}
// Checkout Responses - Employee can create, Admin can read
match /checkout_responses/{responseId} {
allow read: if isAdmin() || (isAuthenticated() && resource.data.userId == request.auth.uid);
allow create: if isAuthenticated() && request.auth.uid == resource.data.userId;
allow update, delete: if isAdmin();
}Mobile Implementation Guide
1. Data Fetching
class CheckoutQuestionsService {
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
Future<List<CheckoutQuestion>> getCheckoutQuestions(String companyId) async {
try {
final snapshot = await _firestore
.collection('checkout_questions')
.where('companyId', isEqualTo: companyId)
.orderBy('createdAt', descending: false)
.get();
return snapshot.docs
.map((doc) => CheckoutQuestion.fromJson({
'id': doc.id,
...doc.data(),
}))
.toList();
} catch (e) {
print('Error fetching checkout questions: $e');
return [];
}
}
}2. UI Implementation
Checkout Questions Screen
class CheckoutQuestionsScreen extends StatefulWidget {
final String userId;
final String companyId;
final String? projectId;
final String? location;
@override
_CheckoutQuestionsScreenState createState() => _CheckoutQuestionsScreenState();
}
class _CheckoutQuestionsScreenState extends State<CheckoutQuestionsScreen> {
List<CheckoutQuestion> questions = [];
Map<String, bool> responses = {};
bool isLoading = true;
bool isSubmitting = false;
@override
void initState() {
super.initState();
_loadQuestions();
}
Future<void> _loadQuestions() async {
final checkoutService = CheckoutQuestionsService();
final fetchedQuestions = await checkoutService.getCheckoutQuestions(widget.companyId);
setState(() {
questions = fetchedQuestions;
// Initialize responses with default answers
responses = Map.fromIterable(
questions,
key: (q) => q.id!,
value: (q) => q.defaultAnswer,
);
isLoading = false;
});
}
@override
Widget build(BuildContext context) {
if (isLoading) {
return Scaffold(
appBar: AppBar(title: Text('Checkout Questions')),
body: Center(child: CircularProgressIndicator()),
);
}
return Scaffold(
appBar: AppBar(
title: Text('Checkout Questions'),
backgroundColor: Colors.blue,
),
body: Column(
children: [
if (questions.isEmpty)
Expanded(
child: Center(
child: Text(
'No checkout questions configured.\nYou can proceed with checkout.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 16, color: Colors.grey),
),
),
)
else
Expanded(
child: ListView.builder(
padding: EdgeInsets.all(16),
itemCount: questions.length,
itemBuilder: (context, index) {
final question = questions[index];
return _buildQuestionCard(question);
},
),
),
_buildActionButtons(),
],
),
);
}
Widget _buildQuestionCard(CheckoutQuestion question) {
return Card(
margin: EdgeInsets.only(bottom: 16),
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
question.question,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 8),
Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: _getDocTypeColor(question.docType),
borderRadius: BorderRadius.circular(12),
),
child: Text(
question.docType.title,
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildAnswerButton(question.id!, true, 'Yes'),
),
SizedBox(width: 12),
Expanded(
child: _buildAnswerButton(question.id!, false, 'No'),
),
],
),
],
),
),
);
}
Widget _buildAnswerButton(String questionId, bool value, String label) {
final isSelected = responses[questionId] == value;
return GestureDetector(
onTap: () {
setState(() {
responses[questionId] = value;
});
},
child: Container(
padding: EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: isSelected ? Colors.blue : Colors.grey.shade200,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isSelected ? Colors.blue : Colors.grey.shade300,
width: 2,
),
),
child: Center(
child: Text(
label,
style: TextStyle(
color: isSelected ? Colors.white : Colors.grey.shade700,
fontWeight: FontWeight.w600,
),
),
),
),
);
}
Color _getDocTypeColor(DocType docType) {
switch (docType) {
case DocType.incident:
return Colors.red;
case DocType.service:
return Colors.orange;
case DocType.inspect:
return Colors.purple;
case DocType.materialFieldUsage:
return Colors.green;
default:
return Colors.blue;
}
}
Widget _buildActionButtons() {
return Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
spreadRadius: 1,
blurRadius: 4,
offset: Offset(0, -2),
),
],
),
child: Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: isSubmitting ? null : () => Navigator.pop(context),
child: Text('Cancel'),
style: OutlinedButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 16),
),
),
),
SizedBox(width: 16),
Expanded(
child: ElevatedButton(
onPressed: isSubmitting ? null : _submitResponses,
child: isSubmitting
? Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
),
SizedBox(width: 8),
Text('Submitting...'),
],
)
: Text('Complete Checkout'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
padding: EdgeInsets.symmetric(vertical: 16),
),
),
),
],
),
);
}
Future<void> _submitResponses() async {
setState(() => isSubmitting = true);
try {
final batch = FirebaseFirestore.instance.batch();
final timestamp = DateTime.now();
for (final question in questions) {
final response = CheckoutResponse(
userId: widget.userId,
questionId: question.id!,
question: question.question,
response: responses[question.id!]!,
docType: question.docType,
timestamp: timestamp,
companyId: widget.companyId,
projectId: widget.projectId,
location: widget.location,
);
final docRef = FirebaseFirestore.instance
.collection('checkout_responses')
.doc();
batch.set(docRef, response.toJson());
}
await batch.commit();
// Navigate back with success
Navigator.pop(context, true);
} catch (e) {
print('Error submitting responses: $e');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to submit responses. Please try again.'),
backgroundColor: Colors.red,
),
);
} finally {
setState(() => isSubmitting = false);
}
}
}3. Integration with Clock-Out Flow
class ClockOutService {
Future<bool> performClockOut({
required String userId,
required String companyId,
String? projectId,
String? location,
}) async {
// 1. Check if there are checkout questions
final checkoutService = CheckoutQuestionsService();
final questions = await checkoutService.getCheckoutQuestions(companyId);
if (questions.isNotEmpty) {
// 2. Show checkout questions screen
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CheckoutQuestionsScreen(
userId: userId,
companyId: companyId,
projectId: projectId,
location: location,
),
),
);
// 3. If user didn't complete questions, cancel clock-out
if (result != true) {
return false;
}
}
// 4. Proceed with normal clock-out process
return await _performActualClockOut(userId, projectId);
}
Future<bool> _performActualClockOut(String userId, String? projectId) async {
// Your existing clock-out logic here
// Update timecard, calculate hours, etc.
return true;
}
}Admin Dashboard Integration
Viewing Responses
Admins can view all checkout responses in the admin dashboard:
class CheckoutResponsesScreen extends StatelessWidget {
Future<List<CheckoutResponse>> _fetchResponses(String companyId) async {
final snapshot = await FirebaseFirestore.instance
.collection('checkout_responses')
.where('companyId', isEqualTo: companyId)
.orderBy('timestamp', descending: true)
.limit(100)
.get();
return snapshot.docs
.map((doc) => CheckoutResponse.fromJson({
'id': doc.id,
...doc.data(),
}))
.toList();
}
@override
Widget build(BuildContext context) {
// Implementation for viewing responses in admin dashboard
// This could be a new tab in the admin console
}
}Best Practices
1. Question Design
- Keep questions clear and concise
- Use yes/no format for easy answering
- Categorize questions by document type for better organization
- Avoid leading questions
2. Performance Optimization
- Cache questions locally to reduce Firebase calls
- Implement offline support for areas with poor connectivity
- Use batch operations when submitting multiple responses
- Implement pagination for large response datasets
3. User Experience
- Show question categories with color-coded badges
- Provide clear visual feedback for selected answers
- Display progress indicator during submission
- Handle errors gracefully with retry options
4. Data Management
- Implement automatic cleanup for old responses (e.g., keep last 90 days)
- Add data export functionality for compliance reporting
- Consider response analytics for identifying trends
- Implement response validation to ensure data quality
Security Considerations
- Authentication: Ensure users are authenticated before accessing questions
- Authorization: Validate user permissions for company access
- Data Validation: Validate all inputs before saving to Firebase
- Rate Limiting: Implement rate limiting to prevent abuse
- Audit Logging: Log all question modifications for compliance
Error Handling
Common Scenarios
- No Network Connection: Cache questions and allow offline completion
- Firebase Errors: Provide retry mechanisms with exponential backoff
- Validation Errors: Clear error messages for users
- Timeout Issues: Implement reasonable timeout values
- Permission Errors: Graceful degradation for insufficient permissions
Testing Strategy
Unit Tests
- Question fetching logic
- Response submission logic
- Data validation functions
- Error handling scenarios
Integration Tests
- End-to-end checkout flow
- Firebase integration
- Network failure scenarios
- Permission edge cases
UI Tests
- Question display functionality
- Answer selection behavior
- Form submission flow
- Error state handling
Future Enhancements
- Multi-language Support: Translate questions based on user locale
- Conditional Questions: Show questions based on previous answers
- Rich Media Support: Allow images or voice recordings as responses
- Analytics Dashboard: Provide insights on response patterns
- Custom Question Types: Support multiple choice, text input, etc.
- Integration with Reports: Link responses to existing report types
- Push Notifications: Remind users to complete missed questions
- Bulk Import: Allow admins to import questions from CSV/Excel
Migration Guide
From Manual Processes
- Identify current checkout procedures
- Convert manual checklists to digital questions
- Train staff on new digital process
- Gradually phase out paper-based systems
Data Migration
- Export existing question formats
- Map to new data structure
- Test migration process
- Perform migration during low-usage period
This comprehensive documentation provides everything needed to implement the checkout questions feature on the mobile side, ensuring seamless integration with the admin console and maintaining data consistency across the platform.