My App

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:

  1. Fetch Questions: Retrieve all relevant checkout questions from Firebase
  2. Display Questions: Present questions in a user-friendly interface
  3. Collect Answers: Allow users to answer each question
  4. Submit Responses: Save responses to Firebase for admin review
  5. 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 Report
  • service - Service Request
  • addService - Add Service Request
  • hauling - Hauling Log
  • materialFieldUsage - Material Field Usage
  • materialShopping - Material Shopping
  • inspect - Inspection Report
  • offTime - Time Off
  • documents - Document
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

  1. Authentication: Ensure users are authenticated before accessing questions
  2. Authorization: Validate user permissions for company access
  3. Data Validation: Validate all inputs before saving to Firebase
  4. Rate Limiting: Implement rate limiting to prevent abuse
  5. Audit Logging: Log all question modifications for compliance

Error Handling

Common Scenarios

  1. No Network Connection: Cache questions and allow offline completion
  2. Firebase Errors: Provide retry mechanisms with exponential backoff
  3. Validation Errors: Clear error messages for users
  4. Timeout Issues: Implement reasonable timeout values
  5. 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

  1. Multi-language Support: Translate questions based on user locale
  2. Conditional Questions: Show questions based on previous answers
  3. Rich Media Support: Allow images or voice recordings as responses
  4. Analytics Dashboard: Provide insights on response patterns
  5. Custom Question Types: Support multiple choice, text input, etc.
  6. Integration with Reports: Link responses to existing report types
  7. Push Notifications: Remind users to complete missed questions
  8. Bulk Import: Allow admins to import questions from CSV/Excel

Migration Guide

From Manual Processes

  1. Identify current checkout procedures
  2. Convert manual checklists to digital questions
  3. Train staff on new digital process
  4. Gradually phase out paper-based systems

Data Migration

  1. Export existing question formats
  2. Map to new data structure
  3. Test migration process
  4. 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.

On this page