Files
pad_scanner/docs/plans/2026-05-09-registration-plan.md
Misaka_Company d136dbf947 chore: remove obsolete tests, add design docs and PRD
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-09 14:32:24 +08:00

28 KiB

上架登记模块 Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Replace the generic ScanPage with a shelf-registration (上架登记) module that validates scanned barcodes, binds execution card numbers to warehouse locations, and provides API configuration.

Architecture: Lightweight Flutter app using setState for state management, EventChannel for native barcode scanner, SharedPreferences for config persistence, and http package for API calls. CodeParser provides pure regex-based barcode classification.

Tech Stack: Flutter 3.x, Dart, http, shared_preferences, Android native EventChannel (SEUIC scanner)


Task 1: CodeParser — barcode classification service

Files:

  • Create: lib/services/code_parser.dart
  • Test: test/services/code_parser_test.dart

Step 1: Write the tests

// test/services/code_parser_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:pad_scanner/services/code_parser.dart';

void main() {
  group('CodeParser.parse', () {
    test('identifies zongpaiNo type B', () {
      final result = CodeParser.parse('26B1');
      expect(result.type, CodeType.zongpaiNo);
      expect(result.value, '26B1');
    });

    test('identifies zongpaiNo type C', () {
      final result = CodeParser.parse('26C12');
      expect(result.type, CodeType.zongpaiNo);
      expect(result.value, '26C12');
    });

    test('identifies zongpaiNo type T', () {
      final result = CodeParser.parse('26T3');
      expect(result.type, CodeType.zongpaiNo);
      expect(result.value, '26T3');
    });

    test('identifies zongpaiNo type BW (thermometer, 4 digits)', () {
      final result = CodeParser.parse('26BW0001');
      expect(result.type, CodeType.zongpaiNo);
      expect(result.value, '26BW0001');
    });

    test('identifies zongpaiNo type CW (thermometer, 4 digits)', () {
      final result = CodeParser.parse('26CW0015');
      expect(result.type, CodeType.zongpaiNo);
      expect(result.value, '26CW0015');
    });

    test('rejects BW with non-4-digit serial', () {
      final result = CodeParser.parse('26BW01');
      expect(result.type, CodeType.invalid);
    });

    test('rejects CW with non-4-digit serial', () {
      final result = CodeParser.parse('26CW15');
      expect(result.type, CodeType.invalid);
    });

    test('identifies normal location code', () {
      final result = CodeParser.parse('A01-02-03');
      expect(result.type, CodeType.locationNormal);
      expect(result.value, 'A01-02-03');
    });

    test('identifies transit location code', () {
      final result = CodeParser.parse('TRANS-01');
      expect(result.type, CodeType.locationTransit);
      expect(result.value, 'TRANS-01');
    });

    test('rejects lowercase location code', () {
      final result = CodeParser.parse('a01-02-03');
      expect(result.type, CodeType.invalid);
    });

    test('rejects completely invalid code', () {
      final result = CodeParser.parse('HELLO123');
      expect(result.type, CodeType.invalid);
    });

    test('rejects empty string', () {
      final result = CodeParser.parse('');
      expect(result.type, CodeType.invalid);
    });

    test('isLocation helper returns true for normal location', () {
      expect(CodeParser.isLocation(CodeType.locationNormal), true);
      expect(CodeParser.isLocation(CodeType.locationTransit), true);
      expect(CodeParser.isLocation(CodeType.zongpaiNo), false);
      expect(CodeParser.isLocation(CodeType.invalid), false);
    });

    test('identifies production number format (reserved, still invalid)', () {
      final result = CodeParser.parse('R00001');
      expect(result.type, CodeType.invalid);
    });
  });
}

Step 2: Run tests to verify they fail

Run: flutter test test/services/code_parser_test.dart Expected: FAIL — code_parser.dart does not exist

Step 3: Write the implementation

// lib/services/code_parser.dart
/// Regex-based barcode classifier per PRD §4.
class CodeParser {
  // 总排号: YY + B/C/T + digits, or YY + BW/CW + exactly 4 digits
  static final _zongpaiRegex = RegExp(
    r'^\d{2}(B|C|T)\d+$|^\d{2}(BW|CW)\d{4}$',
  );

  // 普通货位: AREA-LEVEL-SLOT, all uppercase letters/digits separated by dashes
  static final _locationRegex = RegExp(
    r'^[A-Z0-9]+-[A-Z0-9]+-[A-Z0-9]+$',
  );

  // 转运货位: TRANS-xxx
  static final _transitRegex = RegExp(
    r'^TRANS-',
  );

  /// Parse a scanned code string and return its classification.
  static ParseResult parse(String code) {
    if (code.isEmpty) {
      return ParseResult(type: CodeType.invalid, value: code);
    }

    // Check transit first (TRANS- prefix)
    if (_transitRegex.hasMatch(code)) {
      return ParseResult(type: CodeType.locationTransit, value: code);
    }

    // Check zongpai number
    if (_zongpaiRegex.hasMatch(code)) {
      return ParseResult(type: CodeType.zongpaiNo, value: code);
    }

    // Check normal location
    if (_locationRegex.hasMatch(code)) {
      return ParseResult(type: CodeType.locationNormal, value: code);
    }

    return ParseResult(type: CodeType.invalid, value: code);
  }

  /// Returns true if the code type represents any location.
  static bool isLocation(CodeType type) =>
      type == CodeType.locationNormal || type == CodeType.locationTransit;
}

enum CodeType { zongpaiNo, locationNormal, locationTransit, invalid }

class ParseResult {
  final CodeType type;
  final String value;
  ParseResult({required this.type, required this.value});
}

Step 4: Run tests to verify they pass

Run: flutter test test/services/code_parser_test.dart Expected: All PASS

Step 5: Commit

git add lib/services/code_parser.dart test/services/code_parser_test.dart
git commit -m "feat: add CodeParser for barcode classification per PRD §4"

Task 2: ApiService — rewrite for registration API

Files:

  • Modify: lib/services/api_service.dart (full rewrite)
  • Delete: lib/models/scan_record.dart (no longer needed)

Step 1: Write the new ApiService

// lib/services/api_service.dart
import 'dart:convert';
import 'package:http/http.dart' as http;

/// Result of a registration API call.
class RegistrationResult {
  final bool success;
  final bool isDuplicate;
  final String? errorMessage;
  final Map<String, dynamic>? duplicateInfo;

  RegistrationResult({
    required this.success,
    this.isDuplicate = false,
    this.errorMessage,
    this.duplicateInfo,
  });

  factory RegistrationResult.ok() =>
      RegistrationResult(success: true);

  factory RegistrationResult.duplicate(Map<String, dynamic> info) =>
      RegistrationResult(success: false, isDuplicate: true, duplicateInfo: info);

  factory RegistrationResult.error(String message) =>
      RegistrationResult(success: false, errorMessage: message);
}

class ApiService {
  final http.Client _client;
  final Duration timeout;

  ApiService({http.Client? client, this.timeout = const Duration(seconds: 5)})
      : _client = client ?? http.Client();

  /// Submit a shelf registration (上架登记).
  Future<RegistrationResult> registerLocation({
    required String baseUrl,
    required String zongpaiNo,
    required String locationCode,
  }) async {
    final uri = Uri.parse('$baseUrl/CargoTrace/location');
    try {
      final response = await _client
          .post(
            uri,
            headers: {'Content-Type': 'application/json'},
            body: jsonEncode({
              'zongpai_no': zongpaiNo,
              'location_code': locationCode,
            }),
          )
          .timeout(timeout);

      switch (response.statusCode) {
        case 200:
          return RegistrationResult.ok();
        case 409:
          final body = jsonDecode(response.body) as Map<String, dynamic>;
          return RegistrationResult.duplicate(body);
        default:
          final body = jsonDecode(response.body) as Map<String, dynamic>;
          final msg = body['error'] ?? body['message'] ?? 'Unknown error (${response.statusCode})';
          return RegistrationResult.error(msg.toString());
      }
    } catch (e) {
      return RegistrationResult.error('网络异常,请检查网络连接');
    }
  }

  /// Test connectivity by making a HEAD request to the base URL.
  Future<bool> testConnection(String baseUrl) async {
    try {
      final uri = Uri.parse(baseUrl);
      final response = await _client.head(uri).timeout(timeout);
      return response.statusCode < 500;
    } catch (_) {
      return false;
    }
  }
}

Step 2: Verify compilation

Run: flutter analyze lib/services/api_service.dart Expected: No issues

Step 3: Commit

git add lib/services/api_service.dart
git rm lib/models/scan_record.dart
git commit -m "feat: rewrite ApiService for registration API, remove ScanRecord model"

Task 3: SettingsPage — rewrite with save + test connection

Files:

  • Modify: lib/pages/settings_page.dart (full rewrite)

Step 1: Write the new SettingsPage

// lib/pages/settings_page.dart
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:pad_scanner/services/api_service.dart';

class SettingsPage extends StatefulWidget {
  const SettingsPage({super.key});

  @override
  State<SettingsPage> createState() => _SettingsPageState();
}

class _SettingsPageState extends State<SettingsPage> {
  final _controller = TextEditingController();
  final _apiService = ApiService();
  bool _saving = false;
  bool _testing = false;

  @override
  void initState() {
    super.initState();
    _loadUrl();
  }

  Future<void> _loadUrl() async {
    final prefs = await SharedPreferences.getInstance();
    final url = prefs.getString('api_url') ?? '';
    _controller.text = url;
  }

  Future<void> _saveUrl() async {
    final url = _controller.text.trim();
    if (url.isEmpty) {
      _showSnackBar('请输入 API 地址', isError: true);
      return;
    }
    setState(() => _saving = true);
    final prefs = await SharedPreferences.getInstance();
    await prefs.setString('api_url', url);
    setState(() => _saving = false);
    if (mounted) {
      _showSnackBar('设置已保存');
    }
  }

  Future<void> _testConnection() async {
    final url = _controller.text.trim();
    if (url.isEmpty) {
      _showSnackBar('请先输入 API 地址', isError: true);
      return;
    }
    setState(() => _testing = true);
    final ok = await _apiService.testConnection(url);
    setState(() => _testing = false);
    if (mounted) {
      _showSnackBar(
        ok ? '连接成功' : '连接失败,请检查地址和网络',
        isError: !ok,
      );
    }
  }

  void _showSnackBar(String message, {bool isError = false}) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(message),
        backgroundColor: isError ? Colors.red.shade700 : Colors.green.shade700,
        duration: const Duration(seconds: 2),
      ),
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('设置')),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            const Text('API 服务器地址', style: TextStyle(fontSize: 14)),
            const SizedBox(height: 8),
            TextField(
              controller: _controller,
              decoration: const InputDecoration(
                hintText: 'http://192.168.1.100:8000',
                border: OutlineInputBorder(),
              ),
              keyboardType: TextInputType.url,
            ),
            const SizedBox(height: 20),
            ElevatedButton.icon(
              onPressed: _saving ? null : _saveUrl,
              icon: _saving
                  ? const SizedBox(
                      width: 18,
                      height: 18,
                      child: CircularProgressIndicator(strokeWidth: 2),
                    )
                  : const Icon(Icons.save),
              label: const Text('保存设置'),
            ),
            const SizedBox(height: 8),
            OutlinedButton.icon(
              onPressed: _testing ? null : _testConnection,
              icon: _testing
                  ? const SizedBox(
                      width: 18,
                      height: 18,
                      child: CircularProgressIndicator(strokeWidth: 2),
                    )
                  : const Icon(Icons.wifi),
              label: const Text('测试连接'),
            ),
          ],
        ),
      ),
    );
  }
}

Step 2: Verify compilation

Run: flutter analyze lib/pages/settings_page.dart Expected: No issues

Step 3: Commit

git add lib/pages/settings_page.dart
git commit -m "feat: rewrite SettingsPage with save and test-connection"

Task 4: RegistrationPage — main shelf registration UI

Files:

  • Create: lib/pages/registration_page.dart

Step 1: Write the RegistrationPage

This is the main page. It handles:

  • Scanner event stream subscription
  • Barcode classification via CodeParser
  • State management for zongpaiNo, locationCode, lock mode
  • Submit flow with error handling
  • PRD §8 layout
// lib/pages/registration_page.dart
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:pad_scanner/services/scanner_service.dart';
import 'package:pad_scanner/services/code_parser.dart';
import 'package:pad_scanner/services/api_service.dart';
import 'package:pad_scanner/pages/settings_page.dart';

class RegistrationPage extends StatefulWidget {
  const RegistrationPage({super.key});

  @override
  State<RegistrationPage> createState() => _RegistrationPageState();
}

class _RegistrationPageState extends State<RegistrationPage> {
  final _scannerService = ScannerService();
  final _apiService = ApiService();

  String? _zongpaiNo;
  String? _locationCode;
  CodeType? _locationType;
  bool _isLocked = false;
  bool _isSubmitting = false;

  // Feedback state
  String? _successMessage;
  String? _snackbarMessage;
  Color? _snackbarColor;

  @override
  void initState() {
    super.initState();
    _scannerService.scanResults.listen(_onScan);
  }

  void _onScan(ScanResult result) {
    final parsed = CodeParser.parse(result.barcode);
    switch (parsed.type) {
      case CodeType.zongpaiNo:
        setState(() {
          _zongpaiNo = parsed.value;
          _snackbarMessage = null;
          _successMessage = null;
        });
      case CodeType.locationNormal:
      case CodeType.locationTransit:
        if (!_isLocked) {
          setState(() {
            _locationCode = parsed.value;
            _locationType = parsed.type;
            _snackbarMessage = null;
            _successMessage = null;
          });
        }
      case CodeType.invalid:
        _showFeedback('无效码,请重新扫描', isError: true);
    }
  }

  void _showFeedback(String message, {bool isError = false}) {
    setState(() {
      _snackbarMessage = message;
      _snackbarColor = isError ? Colors.red.shade700 : Colors.green.shade700;
    });
    Future.delayed(const Duration(seconds: 2), () {
      if (mounted) setState(() => _snackbarMessage = null);
    });
  }

  String get _statusText {
    if (_isSubmitting) return '正在提交…';
    if (_successMessage != null) return _successMessage!;
    if (_isLocked && _locationCode != null && _zongpaiNo == null) {
      return '货位已锁定,请扫描下一张执行卡';
    }
    final hasZ = _zongpaiNo != null;
    final hasL = _locationCode != null;
    if (hasZ && hasL) return '请确认信息并提交';
    if (hasZ && !hasL) return '请扫描目标货位号';
    if (!hasZ && hasL) return '请扫描执行卡';
    return '等待扫描总排号或货位号…';
  }

  bool get _canSubmit =>
      _zongpaiNo != null &&
      _locationCode != null &&
      !_isSubmitting;

  Future<void> _submit() async {
    if (!_canSubmit) return;

    final prefs = await SharedPreferences.getInstance();
    final baseUrl = prefs.getString('api_url') ?? '';
    if (baseUrl.isEmpty) {
      _showFeedback('未配置 API 地址,请前往设置', isError: true);
      return;
    }

    setState(() => _isSubmitting = true);

    final result = await _apiService.registerLocation(
      baseUrl: baseUrl,
      zongpaiNo: _zongpaiNo!,
      locationCode: _locationCode!,
    );

    if (!mounted) return;

    setState(() => _isSubmitting = false);

    if (result.success) {
      setState(() {
        _zongpaiNo = null;
        if (!_isLocked) {
          _locationCode = null;
          _locationType = null;
        }
        _successMessage = _isLocked
            ? '货位已锁定,请扫描下一张执行卡'
            : '上架成功';
      });
      _showFeedback(
        _isLocked ? '上架成功' : '上架成功',
        isError: false,
      );
      // Auto-clear success message
      Future.delayed(const Duration(milliseconds: 1500), () {
        if (mounted) setState(() => _successMessage = null);
      });
    } else if (result.isDuplicate) {
      _showDuplicateDialog(result.duplicateInfo);
    } else {
      _showFeedback(result.errorMessage ?? '提交失败', isError: true);
    }
  }

  void _showDuplicateDialog(Map<String, dynamic>? info) {
    showDialog(
      context: context,
      barrierDismissible: false,
      builder: (ctx) => AlertDialog(
        backgroundColor: Colors.red.shade50,
        title: const Row(
          children: [
            Icon(Icons.warning, color: Colors.red),
            SizedBox(width: 8),
            Text('重复上架', style: TextStyle(color: Colors.red)),
          ],
        ),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('总排号:${_zongpaiNo ?? ""}'),
            const SizedBox(height: 4),
            Text('已登记货位:${info?["location_code"] ?? "未知"}'),
            const SizedBox(height: 4),
            Text('登记时间:${info?["registered_at"] ?? "未知"}'),
            const SizedBox(height: 12),
            const Text('请核查实物,确认是否操作错误。', style: TextStyle(fontWeight: FontWeight.bold)),
          ],
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(ctx),
            child: const Text('关闭'),
          ),
        ],
      ),
    );
  }

  void _toggleLock(bool value) {
    if (value && _locationCode == null) {
      _showFeedback('请先扫描货位号', isError: true);
      return;
    }
    setState(() => _isLocked = value);
  }

  String _locationLabel(CodeType? type) {
    if (type == CodeType.locationTransit) return '转运区域';
    return '普通货架';
  }

  Color _locationLabelColor(CodeType? type) {
    if (type == CodeType.locationTransit) return Colors.orange;
    return Colors.blue;
  }

  @override
  Widget build(BuildContext context) {
    final colorScheme = Theme.of(context).colorScheme;

    return Scaffold(
      appBar: AppBar(
        title: const Text('上架登记'),
        actions: [
          // Lock toggle in app bar
          Padding(
            padding: const EdgeInsets.only(right: 4),
            child: Row(
              children: [
                const Text('锁定货位', style: TextStyle(fontSize: 13)),
                Switch(
                  value: _isLocked,
                  onChanged: _toggleLock,
                ),
                IconButton(
                  icon: const Icon(Icons.settings),
                  onPressed: () => Navigator.push(
                    context,
                    MaterialPageRoute(builder: (_) => const SettingsPage()),
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
      body: Column(
        children: [
          // Feedback snackbar area
          if (_snackbarMessage != null)
            Container(
              width: double.infinity,
              padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16),
              color: _snackbarColor,
              child: Text(
                _snackbarMessage!,
                style: const TextStyle(color: Colors.white, fontSize: 14),
                textAlign: TextAlign.center,
              ),
            ),

          // Main form area
          Expanded(
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                children: [
                  // Zongpai number field
                  const Align(
                    alignment: Alignment.centerLeft,
                    child: Text('总排号', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
                  ),
                  const SizedBox(height: 6),
                  Container(
                    width: double.infinity,
                    padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
                    decoration: BoxDecoration(
                      border: Border.all(
                        color: _zongpaiNo != null ? Colors.green : Colors.grey.shade400,
                        width: _zongpaiNo != null ? 2 : 1,
                      ),
                      borderRadius: BorderRadius.circular(8),
                    ),
                    child: Text(
                      _zongpaiNo ?? '',
                      style: TextStyle(
                        fontSize: 20,
                        fontWeight: FontWeight.bold,
                        color: _zongpaiNo != null ? Colors.black87 : Colors.grey,
                      ),
                    ),
                  ),

                  const SizedBox(height: 20),

                  // Location field
                  Row(
                    children: [
                      const Text('目标货位', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
                      if (_locationCode != null) ...[
                        const SizedBox(width: 8),
                        Container(
                          padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
                          decoration: BoxDecoration(
                            color: _locationLabelColor(_locationType).withValues(alpha: 0.15),
                            borderRadius: BorderRadius.circular(4),
                          ),
                          child: Text(
                            _locationLabel(_locationType),
                            style: TextStyle(
                              fontSize: 11,
                              color: _locationLabelColor(_locationType),
                              fontWeight: FontWeight.bold,
                            ),
                          ),
                        ),
                      ],
                    ],
                  ),
                  const SizedBox(height: 6),
                  Container(
                    width: double.infinity,
                    padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
                    decoration: BoxDecoration(
                      border: Border.all(
                        color: _locationCode != null ? Colors.green : Colors.grey.shade400,
                        width: _locationCode != null ? 2 : 1,
                      ),
                      borderRadius: BorderRadius.circular(8),
                    ),
                    child: Row(
                      children: [
                        Expanded(
                          child: Text(
                            _locationCode ?? '',
                            style: TextStyle(
                              fontSize: 20,
                              fontWeight: FontWeight.bold,
                              color: _locationCode != null ? Colors.black87 : Colors.grey,
                            ),
                          ),
                        ),
                        if (_isLocked)
                          const Icon(Icons.lock, color: Colors.orange, size: 20),
                      ],
                    ),
                  ),

                  const SizedBox(height: 28),

                  // Submit button
                  SizedBox(
                    width: double.infinity,
                    height: 48,
                    child: ElevatedButton(
                      onPressed: _canSubmit ? _submit : null,
                      style: ElevatedButton.styleFrom(
                        backgroundColor: _canSubmit
                            ? colorScheme.primary
                            : Colors.grey.shade300,
                        foregroundColor: _canSubmit
                            ? Colors.white
                            : Colors.grey.shade600,
                        shape: RoundedRectangleBorder(
                          borderRadius: BorderRadius.circular(8),
                        ),
                      ),
                      child: _isSubmitting
                          ? const SizedBox(
                              width: 22,
                              height: 22,
                              child: CircularProgressIndicator(
                                strokeWidth: 2,
                                color: Colors.white,
                              ),
                            )
                          : const Text('确 认 上 架', style: TextStyle(fontSize: 18)),
                    ),
                  ),
                ],
              ),
            ),
          ),

          // Status bar at bottom
          Container(
            width: double.infinity,
            padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
            color: colorScheme.surfaceContainerHighest,
            child: Row(
              children: [
                Container(
                  width: 8,
                  height: 8,
                  decoration: BoxDecoration(
                    color: _isSubmitting
                        ? Colors.orange
                        : _successMessage != null
                            ? Colors.green
                            : Colors.blue,
                    shape: BoxShape.circle,
                  ),
                ),
                const SizedBox(width: 8),
                Expanded(
                  child: Text(
                    _statusText,
                    style: const TextStyle(fontSize: 14),
                    overflow: TextOverflow.ellipsis,
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

Step 2: Verify compilation

Run: flutter analyze lib/pages/registration_page.dart Expected: No issues

Step 3: Commit

git add lib/pages/registration_page.dart
git commit -m "feat: add RegistrationPage with single and lock-mode shelf registration"

Task 5: main.dart — wire up RegistrationPage as home

Files:

  • Modify: lib/main.dart
  • Delete: lib/pages/scan_page.dart

Step 1: Update main.dart

// lib/main.dart
import 'package:flutter/material.dart';
import 'package:pad_scanner/pages/registration_page.dart';

void main() {
  runApp(const PadScannerApp());
}

class PadScannerApp extends StatelessWidget {
  const PadScannerApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '上架登记',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
        useMaterial3: true,
      ),
      home: const RegistrationPage(),
    );
  }
}

Step 2: Delete old ScanPage

git rm lib/pages/scan_page.dart

Step 3: Verify full project compiles

Run: flutter analyze Expected: No issues

Step 4: Commit

git add lib/main.dart
git commit -m "feat: wire RegistrationPage as app home, remove ScanPage"

Task 6: Final verification

Step 1: Run all tests

Run: flutter test Expected: All tests pass

Step 2: Run full analysis

Run: flutter analyze Expected: No issues found

Step 3: Verify no dangling imports

Search for any remaining references to scan_page.dart or scan_record.dart: Run: grep -r "scan_page\|scan_record" lib/ Expected: No matches