Files
minhthu/CLAUDE.md
2025-10-28 00:09:46 +07:00

29 KiB

Flutter Warehouse Management App Guidelines

App Overview

Warehouse management app for importing and exporting products with authentication.

App Flow

  1. Login Screen: User authentication → Store access token
  2. Select Warehouse Screen: Choose warehouse from list
  3. Operation Selection Screen: Choose Import or Export products
  4. Product List Screen: Display products based on operation type

Project Structure

lib/
  core/
    constants/
      api_endpoints.dart
    theme/
      app_theme.dart
    widgets/
      custom_button.dart
      loading_indicator.dart
    network/
      api_client.dart
      api_response.dart
    storage/
      secure_storage.dart
  features/
    auth/
      data/
        datasources/
          auth_remote_datasource.dart
        models/
          login_request_model.dart
          login_response_model.dart
        repositories/
          auth_repository_impl.dart
      domain/
        entities/
          user_entity.dart
        repositories/
          auth_repository.dart
        usecases/
          login_usecase.dart
      presentation/
        providers/
          auth_provider.dart
        pages/
          login_page.dart
        widgets/
          login_form.dart
    warehouse/
      data/
        datasources/
          warehouse_remote_datasource.dart
        models/
          warehouse_model.dart
        repositories/
          warehouse_repository_impl.dart
      domain/
        entities/
          warehouse_entity.dart
        repositories/
          warehouse_repository.dart
        usecases/
          get_warehouses_usecase.dart
      presentation/
        providers/
          warehouse_provider.dart
        pages/
          warehouse_selection_page.dart
        widgets/
          warehouse_card.dart
    operation/
      presentation/
        pages/
          operation_selection_page.dart
        widgets/
          operation_card.dart
    products/
      data/
        datasources/
          products_remote_datasource.dart
        models/
          product_model.dart
        repositories/
          products_repository_impl.dart
      domain/
        entities/
          product_entity.dart
        repositories/
          products_repository.dart
        usecases/
          get_products_usecase.dart
      presentation/
        providers/
          products_provider.dart
        pages/
          products_page.dart
        widgets/
          product_list_item.dart
  main.dart

API Response Format

All API responses follow this structure:

class ApiResponse<T> {
  final T? value;
  final bool isSuccess;
  final bool isFailure;
  final List<String> errors;
  final List<String> errorCodes;

  ApiResponse({
    this.value,
    required this.isSuccess,
    required this.isFailure,
    this.errors = const [],
    this.errorCodes = const [],
  });

  factory ApiResponse.fromJson(
    Map<String, dynamic> json,
    T Function(dynamic)? fromJsonT,
  ) {
    return ApiResponse(
      value: json['Value'] != null && fromJsonT != null
          ? fromJsonT(json['Value'])
          : json['Value'],
      isSuccess: json['IsSuccess'] ?? false,
      isFailure: json['IsFailure'] ?? false,
      errors: List<String>.from(json['Errors'] ?? []),
      errorCodes: List<String>.from(json['ErrorCodes'] ?? []),
    );
  }
}

Data Models

User Model

class User {
  final String userId;
  final String username;
  final String accessToken;
  final String? refreshToken;

  User({
    required this.userId,
    required this.username,
    required this.accessToken,
    this.refreshToken,
  });

  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      userId: json['userId'],
      username: json['username'],
      accessToken: json['accessToken'],
      refreshToken: json['refreshToken'],
    );
  }
}

Warehouse Model

class Warehouse {
  final int id;
  final String name;
  final String code;
  final String? description;
  final bool isNGWareHouse;
  final int totalCount;

  Warehouse({
    required this.id,
    required this.name,
    required this.code,
    this.description,
    required this.isNGWareHouse,
    required this.totalCount,
  });

  factory Warehouse.fromJson(Map<String, dynamic> json) {
    return Warehouse(
      id: json['Id'] ?? 0,
      name: json['Name'] ?? '',
      code: json['Code'] ?? '',
      description: json['Description'],
      isNGWareHouse: json['IsNGWareHouse'] ?? false,
      totalCount: json['TotalCount'] ?? 0,
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'Id': id,
      'Name': name,
      'Code': code,
      'Description': description,
      'IsNGWareHouse': isNGWareHouse,
      'TotalCount': totalCount,
    };
  }
}

Product Model

class Product {
  final int id;
  final String name;
  final String code;
  final String fullName;
  final String? description;
  final String? lotCode;
  final String? lotNumber;
  final String? logo;
  final String? barcode;

  // Quantity fields
  final int quantity;
  final int totalQuantity;
  final int passedQuantity;
  final double? passedQuantityWeight;
  final int issuedQuantity;
  final double? issuedQuantityWeight;
  final int piecesInStock;
  final double weightInStock;

  // Weight and pieces
  final double weight;
  final int pieces;
  final double conversionRate;
  final double? percent;

  // Price and status
  final double? price;
  final bool isActive;
  final bool isConfirm;
  final int? productStatusId;
  final int productTypeId;

  // Relations
  final int? orderId;
  final int? parentId;
  final int? receiverStageId;
  final dynamic order;

  // Dates
  final String? startDate;
  final String? endDate;

  // Lists
  final List<dynamic> productions;
  final List<dynamic> customerProducts;
  final List<dynamic> productStages;
  final dynamic childrenProducts;
  final dynamic productStageWareHouses;
  final dynamic productStageDetailWareHouses;
  final dynamic productExportExcelSheetDataModels;
  final dynamic materialLabels;
  final dynamic materials;
  final dynamic images;
  final dynamic attachmentFiles;

  Product({
    required this.id,
    required this.name,
    required this.code,
    required this.fullName,
    this.description,
    this.lotCode,
    this.lotNumber,
    this.logo,
    this.barcode,
    required this.quantity,
    required this.totalQuantity,
    required this.passedQuantity,
    this.passedQuantityWeight,
    required this.issuedQuantity,
    this.issuedQuantityWeight,
    required this.piecesInStock,
    required this.weightInStock,
    required this.weight,
    required this.pieces,
    required this.conversionRate,
    this.percent,
    this.price,
    required this.isActive,
    required this.isConfirm,
    this.productStatusId,
    required this.productTypeId,
    this.orderId,
    this.parentId,
    this.receiverStageId,
    this.order,
    this.startDate,
    this.endDate,
    this.productions = const [],
    this.customerProducts = const [],
    this.productStages = const [],
    this.childrenProducts,
    this.productStageWareHouses,
    this.productStageDetailWareHouses,
    this.productExportExcelSheetDataModels,
    this.materialLabels,
    this.materials,
    this.images,
    this.attachmentFiles,
  });

  factory Product.fromJson(Map<String, dynamic> json) {
    return Product(
      id: json['Id'] ?? 0,
      name: json['Name'] ?? '',
      code: json['Code'] ?? '',
      fullName: json['FullName'] ?? '',
      description: json['Description'],
      lotCode: json['LotCode'],
      lotNumber: json['LotNumber'],
      logo: json['Logo'],
      barcode: json['Barcode'],
      quantity: json['Quantity'] ?? 0,
      totalQuantity: json['TotalQuantity'] ?? 0,
      passedQuantity: json['PassedQuantity'] ?? 0,
      passedQuantityWeight: json['PassedQuantityWeight']?.toDouble(),
      issuedQuantity: json['IssuedQuantity'] ?? 0,
      issuedQuantityWeight: json['IssuedQuantityWeight']?.toDouble(),
      piecesInStock: json['PiecesInStock'] ?? 0,
      weightInStock: (json['WeightInStock'] ?? 0).toDouble(),
      weight: (json['Weight'] ?? 0).toDouble(),
      pieces: json['Pieces'] ?? 0,
      conversionRate: (json['ConversionRate'] ?? 0).toDouble(),
      percent: json['Percent']?.toDouble(),
      price: json['Price']?.toDouble(),
      isActive: json['IsActive'] ?? true,
      isConfirm: json['IsConfirm'] ?? false,
      productStatusId: json['ProductStatusId'],
      productTypeId: json['ProductTypeId'] ?? 0,
      orderId: json['OrderId'],
      parentId: json['ParentId'],
      receiverStageId: json['ReceiverStageId'],
      order: json['Order'],
      startDate: json['StartDate'],
      endDate: json['EndDate'],
      productions: json['Productions'] ?? [],
      customerProducts: json['CustomerProducts'] ?? [],
      productStages: json['ProductStages'] ?? [],
      childrenProducts: json['ChildrenProducts'],
      productStageWareHouses: json['ProductStageWareHouses'],
      productStageDetailWareHouses: json['ProductStageDetailWareHouses'],
      productExportExcelSheetDataModels: json['ProductExportExcelSheetDataModels'],
      materialLabels: json['MaterialLabels'],
      materials: json['Materials'],
      images: json['Images'],
      attachmentFiles: json['AttachmentFiles'],
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'Id': id,
      'Name': name,
      'Code': code,
      'FullName': fullName,
      'Description': description,
      'LotCode': lotCode,
      'LotNumber': lotNumber,
      'Logo': logo,
      'Barcode': barcode,
      'Quantity': quantity,
      'TotalQuantity': totalQuantity,
      'PassedQuantity': passedQuantity,
      'PassedQuantityWeight': passedQuantityWeight,
      'IssuedQuantity': issuedQuantity,
      'IssuedQuantityWeight': issuedQuantityWeight,
      'PiecesInStock': piecesInStock,
      'WeightInStock': weightInStock,
      'Weight': weight,
      'Pieces': pieces,
      'ConversionRate': conversionRate,
      'Percent': percent,
      'Price': price,
      'IsActive': isActive,
      'IsConfirm': isConfirm,
      'ProductStatusId': productStatusId,
      'ProductTypeId': productTypeId,
      'OrderId': orderId,
      'ParentId': parentId,
      'ReceiverStageId': receiverStageId,
      'Order': order,
      'StartDate': startDate,
      'EndDate': endDate,
      'Productions': productions,
      'CustomerProducts': customerProducts,
      'ProductStages': productStages,
      'ChildrenProducts': childrenProducts,
      'ProductStageWareHouses': productStageWareHouses,
      'ProductStageDetailWareHouses': productStageDetailWareHouses,
      'ProductExportExcelSheetDataModels': productExportExcelSheetDataModels,
      'MaterialLabels': materialLabels,
      'Materials': materials,
      'Images': images,
      'AttachmentFiles': attachmentFiles,
    };
  }
}

Login Request Model

class LoginRequest {
  final String username;
  final String password;

  LoginRequest({
    required this.username,
    required this.password,
  });

  Map<String, dynamic> toJson() => {
    'username': username,
    'password': password,
  };
}

Screen Layouts

Login Screen

┌─────────────────────────┐
│                         │
│      [App Logo]         │
│                         │
│   Warehouse Manager     │
│                         │
├─────────────────────────┤
│                         │
│ Username: [__________]  │
│                         │
│ Password: [__________]  │
│                         │
│      [Login Button]     │
│                         │
└─────────────────────────┘

Select Warehouse Screen

┌──────────────────────────────────┐
│  Select Warehouse                │
├──────────────────────────────────┤
│                                  │
│ ┌──────────────────────────────┐ │
│ │ Kho nguyên vật liệu          │ │
│ │ Code: 001                    │ │
│ │ Items: 8                     │ │
│ └──────────────────────────────┘ │
│                                  │
│ ┌──────────────────────────────┐ │
│ │ Kho bán thành phẩm công đoạn│ │
│ │ Code: 002                    │ │
│ │ Items: 8                     │ │
│ └──────────────────────────────┘ │
│                                  │
│ ┌──────────────────────────────┐ │
│ │ Kho thành phẩm               │ │
│ │ Code: 003                    │ │
│ │ Items: 8                     │ │
│ └──────────────────────────────┘ │
│                                  │
│ ┌──────────────────────────────┐ │
│ │ Kho tiêu hao                 │ │
│ │ Code: 004                    │ │
│ │ Items: 8 • Để chứa phụ tùng │ │
│ └──────────────────────────────┘ │
│                                  │
└──────────────────────────────────┘

Operation Selection Screen

┌─────────────────────────┐
│  Select Operation       │
│  Warehouse: 001         │
├─────────────────────────┤
│                         │
│                         │
│   ┌─────────────────┐   │
│   │                 │   │
│   │  Import Products│   │
│   │                 │   │
│   └─────────────────┘   │
│                         │
│   ┌─────────────────┐   │
│   │                 │   │
│   │  Export Products│   │
│   │                 │   │
│   └─────────────────┘   │
│                         │
│                         │
└─────────────────────────┘

Products List Screen

┌───────────────────────────────────┐
│  Products (Import)                │
│  Warehouse: Kho nguyên vật liệu   │
├───────────────────────────────────┤
│                                   │
│ • SCM435 | Thép 435               │
│   Code: SCM435                    │
│   Weight: 120.00 | Pieces: 1320   │
│   In Stock: 0 pcs (0.00 kg)       │
│   Conversion Rate: 11.00          │
├───────────────────────────────────┤
│ • SCM440 | Thép 440               │
│   Code: SCM440                    │
│   Weight: 85.50 | Pieces: 950     │
│   In Stock: 150 pcs (12.75 kg)    │
│   Conversion Rate: 11.20          │
├───────────────────────────────────┤
│ • SS304 | Thép không gỉ           │
│   Code: SS304                     │
│   Weight: 200.00 | Pieces: 2000   │
│   In Stock: 500 pcs (50.00 kg)    │
│   Conversion Rate: 10.00          │
├───────────────────────────────────┤
│                                   │
└───────────────────────────────────┘

State Management (Riverpod)

Auth State

class AuthState {
  final User? user;
  final bool isAuthenticated;
  final bool isLoading;
  final String? error;

  AuthState({
    this.user,
    this.isAuthenticated = false,
    this.isLoading = false,
    this.error,
  });

  AuthState copyWith({
    User? user,
    bool? isAuthenticated,
    bool? isLoading,
    String? error,
  }) {
    return AuthState(
      user: user ?? this.user,
      isAuthenticated: isAuthenticated ?? this.isAuthenticated,
      isLoading: isLoading ?? this.isLoading,
      error: error,
    );
  }
}

Warehouse State

class WarehouseState {
  final List<Warehouse> warehouses;
  final Warehouse? selectedWarehouse;
  final bool isLoading;
  final String? error;

  WarehouseState({
    this.warehouses = const [],
    this.selectedWarehouse,
    this.isLoading = false,
    this.error,
  });

  WarehouseState copyWith({
    List<Warehouse>? warehouses,
    Warehouse? selectedWarehouse,
    bool? isLoading,
    String? error,
  }) {
    return WarehouseState(
      warehouses: warehouses ?? this.warehouses,
      selectedWarehouse: selectedWarehouse ?? this.selectedWarehouse,
      isLoading: isLoading ?? this.isLoading,
      error: error,
    );
  }
}

Products State

class ProductsState {
  final List<Product> products;
  final String operationType; // 'import' or 'export'
  final bool isLoading;
  final String? error;

  ProductsState({
    this.products = const [],
    this.operationType = 'import',
    this.isLoading = false,
    this.error,
  });

  ProductsState copyWith({
    List<Product>? products,
    String? operationType,
    bool? isLoading,
    String? error,
  }) {
    return ProductsState(
      products: products ?? this.products,
      operationType: operationType ?? this.operationType,
      isLoading: isLoading ?? this.isLoading,
      error: error,
    );
  }
}

Secure Storage

Token Management

class SecureStorage {
  static const _storage = FlutterSecureStorage();
  static const _accessTokenKey = 'access_token';
  static const _refreshTokenKey = 'refresh_token';

  Future<void> saveAccessToken(String token) async {
    await _storage.write(key: _accessTokenKey, value: token);
  }

  Future<String?> getAccessToken() async {
    return await _storage.read(key: _accessTokenKey);
  }

  Future<void> saveRefreshToken(String token) async {
    await _storage.write(key: _refreshTokenKey, value: token);
  }

  Future<String?> getRefreshToken() async {
    return await _storage.read(key: _refreshTokenKey);
  }

  Future<void> clearAll() async {
    await _storage.deleteAll();
  }
}

API Integration

Available APIs (CURL format)

# Login
curl -X POST https://api.example.com/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username": "user", "password": "pass"}'

# Get Warehouses
curl -X GET https://api.example.com/warehouses \
  -H "Authorization: Bearer {access_token}"

# Get Products
curl -X GET https://api.example.com/products?warehouseId={id}&type={import/export} \
  -H "Authorization: Bearer {access_token}"

Auth Remote Data Source

abstract class AuthRemoteDataSource {
  Future<User> login(LoginRequest request);
}

class AuthRemoteDataSourceImpl implements AuthRemoteDataSource {
  final ApiClient apiClient;

  AuthRemoteDataSourceImpl(this.apiClient);

  @override
  Future<User> login(LoginRequest request) async {
    final response = await apiClient.post(
      '/auth/login',
      data: request.toJson(),
    );

    final apiResponse = ApiResponse.fromJson(
      response.data,
      (json) => User.fromJson(json),
    );

    if (apiResponse.isSuccess && apiResponse.value != null) {
      return apiResponse.value!;
    } else {
      throw ServerException(
        apiResponse.errors.isNotEmpty
          ? apiResponse.errors.first
          : 'Login failed'
      );
    }
  }
}

Warehouse Remote Data Source

abstract class WarehouseRemoteDataSource {
  Future<List<Warehouse>> getWarehouses();
}

class WarehouseRemoteDataSourceImpl implements WarehouseRemoteDataSource {
  final ApiClient apiClient;

  WarehouseRemoteDataSourceImpl(this.apiClient);

  @override
  Future<List<Warehouse>> getWarehouses() async {
    final response = await apiClient.get('/warehouses');

    final apiResponse = ApiResponse.fromJson(
      response.data,
      (json) => (json as List).map((e) => Warehouse.fromJson(e)).toList(),
    );

    if (apiResponse.isSuccess && apiResponse.value != null) {
      return apiResponse.value!;
    } else {
      throw ServerException(
        apiResponse.errors.isNotEmpty
          ? apiResponse.errors.first
          : 'Failed to get warehouses'
      );
    }
  }
}

Products Remote Data Source

abstract class ProductsRemoteDataSource {
  Future<List<Product>> getProducts(int warehouseId, String type);
}

class ProductsRemoteDataSourceImpl implements ProductsRemoteDataSource {
  final ApiClient apiClient;

  ProductsRemoteDataSourceImpl(this.apiClient);

  @override
  Future<List<Product>> getProducts(int warehouseId, String type) async {
    final response = await apiClient.get(
      '/products',
      queryParameters: {
        'warehouseId': warehouseId,
        'type': type,
      },
    );

    final apiResponse = ApiResponse.fromJson(
      response.data,
      (json) => (json as List).map((e) => Product.fromJson(e)).toList(),
    );

    if (apiResponse.isSuccess && apiResponse.value != null) {
      return apiResponse.value!;
    } else {
      throw ServerException(
        apiResponse.errors.isNotEmpty
          ? apiResponse.errors.first
          : 'Failed to get products'
      );
    }
  }
}

Use Cases

Login Use Case

class LoginUseCase {
  final AuthRepository repository;
  final SecureStorage secureStorage;

  LoginUseCase(this.repository, this.secureStorage);

  Future<Either<Failure, User>> call(LoginRequest request) async {
    final result = await repository.login(request);

    return result.fold(
      (failure) => Left(failure),
      (user) async {
        // Save tokens to secure storage
        await secureStorage.saveAccessToken(user.accessToken);
        if (user.refreshToken != null) {
          await secureStorage.saveRefreshToken(user.refreshToken!);
        }
        return Right(user);
      },
    );
  }
}

Get Warehouses Use Case

class GetWarehousesUseCase {
  final WarehouseRepository repository;

  GetWarehousesUseCase(this.repository);

  Future<Either<Failure, List<Warehouse>>> call() async {
    return await repository.getWarehouses();
  }
}

Get Products Use Case

class GetProductsUseCase {
  final ProductsRepository repository;

  GetProductsUseCase(this.repository);

  Future<Either<Failure, List<Product>>> call(
    int warehouseId,
    String type,
  ) async {
    return await repository.getProducts(warehouseId, type);
  }
}

Repository Pattern

Auth Repository

abstract class AuthRepository {
  Future<Either<Failure, User>> login(LoginRequest request);
}

class AuthRepositoryImpl implements AuthRepository {
  final AuthRemoteDataSource remoteDataSource;

  AuthRepositoryImpl(this.remoteDataSource);

  @override
  Future<Either<Failure, User>> login(LoginRequest request) async {
    try {
      final user = await remoteDataSource.login(request);
      return Right(user);
    } on ServerException catch (e) {
      return Left(ServerFailure(e.message));
    } catch (e) {
      return Left(ServerFailure(e.toString()));
    }
  }
}

Warehouse Repository

abstract class WarehouseRepository {
  Future<Either<Failure, List<Warehouse>>> getWarehouses();
}

class WarehouseRepositoryImpl implements WarehouseRepository {
  final WarehouseRemoteDataSource remoteDataSource;

  WarehouseRepositoryImpl(this.remoteDataSource);

  @override
  Future<Either<Failure, List<Warehouse>>> getWarehouses() async {
    try {
      final warehouses = await remoteDataSource.getWarehouses();
      return Right(warehouses);
    } on ServerException catch (e) {
      return Left(ServerFailure(e.message));
    } catch (e) {
      return Left(ServerFailure(e.toString()));
    }
  }
}

Products Repository

abstract class ProductsRepository {
  Future<Either<Failure, List<Product>>> getProducts(
    int warehouseId,
    String type,
  );
}

class ProductsRepositoryImpl implements ProductsRepository {
  final ProductsRemoteDataSource remoteDataSource;

  ProductsRepositoryImpl(this.remoteDataSource);

  @override
  Future<Either<Failure, List<Product>>> getProducts(
    int warehouseId,
    String type,
  ) async {
    try {
      final products = await remoteDataSource.getProducts(warehouseId, type);
      return Right(products);
    } on ServerException catch (e) {
      return Left(ServerFailure(e.message));
    } catch (e) {
      return Left(ServerFailure(e.toString()));
    }
  }
}

Navigation Flow

Router Configuration (go_router)

final router = GoRouter(
  initialLocation: '/login',
  routes: [
    GoRoute(
      path: '/login',
      name: 'login',
      builder: (context, state) => LoginPage(),
    ),
    GoRoute(
      path: '/warehouses',
      name: 'warehouses',
      builder: (context, state) => WarehouseSelectionPage(),
    ),
    GoRoute(
      path: '/operations',
      name: 'operations',
      builder: (context, state) {
        final warehouse = state.extra as Warehouse;
        return OperationSelectionPage(warehouse: warehouse);
      },
    ),
    GoRoute(
      path: '/products',
      name: 'products',
      builder: (context, state) {
        final params = state.extra as Map<String, dynamic>;
        return ProductsPage(
          warehouse: params['warehouse'],
          operationType: params['type'],
        );
      },
    ),
  ],
  redirect: (context, state) async {
    final secureStorage = SecureStorage();
    final token = await secureStorage.getAccessToken();
    final isAuthenticated = token != null;
    final isLoggingIn = state.matchedLocation == '/login';

    if (!isAuthenticated && !isLoggingIn) {
      return '/login';
    }
    if (isAuthenticated && isLoggingIn) {
      return '/warehouses';
    }
    return null;
  },
);

API Client with Interceptor

Dio Configuration

class ApiClient {
  late final Dio _dio;
  final SecureStorage _secureStorage;

  ApiClient(this._secureStorage) {
    _dio = Dio(
      BaseOptions(
        baseUrl: 'https://api.example.com',
        connectTimeout: Duration(seconds: 30),
        receiveTimeout: Duration(seconds: 30),
        headers: {
          'Content-Type': 'application/json',
        },
      ),
    );

    _dio.interceptors.add(
      InterceptorsWrapper(
        onRequest: (options, handler) async {
          // Add token to headers
          final token = await _secureStorage.getAccessToken();
          if (token != null) {
            options.headers['Authorization'] = 'Bearer $token';
          }
          return handler.next(options);
        },
        onError: (error, handler) async {
          // Handle 401 unauthorized
          if (error.response?.statusCode == 401) {
            // Clear tokens and redirect to login
            await _secureStorage.clearAll();
            // Navigate to login
          }
          return handler.next(error);
        },
      ),
    );
  }

  Future<Response> get(
    String path, {
    Map<String, dynamic>? queryParameters,
  }) async {
    return await _dio.get(path, queryParameters: queryParameters);
  }

  Future<Response> post(
    String path, {
    dynamic data,
  }) async {
    return await _dio.post(path, data: data);
  }
}

Dependencies

dependencies:
  flutter_riverpod: ^2.4.9
  go_router: ^12.1.3
  dio: ^5.3.2
  dartz: ^0.10.1
  get_it: ^7.6.4
  flutter_secure_storage: ^9.0.0

dev_dependencies:
  flutter_test: ^3.0.0
  mockito: ^5.4.2
  build_runner: ^2.4.7

Error Handling

abstract class Failure {
  final String message;
  const Failure(this.message);
}

class ServerFailure extends Failure {
  const ServerFailure(String message) : super(message);
}

class NetworkFailure extends Failure {
  const NetworkFailure(String message) : super(message);
}

class AuthenticationFailure extends Failure {
  const AuthenticationFailure(String message) : super(message);
}

class ServerException implements Exception {
  final String message;
  const ServerException(this.message);
}

Key Points

  • Store access token in flutter_secure_storage after successful login
  • All API responses use "Value" key for data
  • API responses follow IsSuccess/IsFailure pattern
  • Add Authorization header with Bearer token to all authenticated requests
  • Handle 401 errors by clearing tokens and redirecting to login
  • Use clean architecture with use cases and repository pattern
  • Navigation flow: Login → Warehouses → Operations → Products
  • Only login, get warehouses, and get products APIs are available currently
  • Other features (import/export operations) will use placeholder/mock data until APIs are ready