update
This commit is contained in:
189
lib/features/home/data/datasources/home_local_datasource.dart
Normal file
189
lib/features/home/data/datasources/home_local_datasource.dart
Normal file
@@ -0,0 +1,189 @@
|
||||
/// Data Source: Home Local Data Source
|
||||
///
|
||||
/// Handles local database operations for home feature using Hive.
|
||||
/// This is the single source of truth for cached home data.
|
||||
///
|
||||
/// Responsibilities:
|
||||
/// - Store and retrieve member card from local database
|
||||
/// - Store and retrieve promotions from local database
|
||||
/// - Handle data expiration and cache invalidation
|
||||
library;
|
||||
|
||||
import 'package:worker/core/errors/exceptions.dart';
|
||||
import 'package:worker/features/home/data/models/member_card_model.dart';
|
||||
import 'package:worker/features/home/data/models/promotion_model.dart';
|
||||
|
||||
/// Home Local Data Source
|
||||
///
|
||||
/// Provides methods to interact with Hive database for home data.
|
||||
///
|
||||
/// Cache strategy:
|
||||
/// - Member card: Single entry, updated on login and refresh
|
||||
/// - Promotions: List entry, updated periodically
|
||||
abstract class HomeLocalDataSource {
|
||||
/// Get cached member card
|
||||
///
|
||||
/// Returns cached [MemberCardModel] if available.
|
||||
/// Throws [CacheException] if no data found or data corrupted.
|
||||
Future<MemberCardModel> getMemberCard();
|
||||
|
||||
/// Cache member card
|
||||
///
|
||||
/// Stores [MemberCardModel] in local database.
|
||||
/// Overwrites existing data.
|
||||
Future<void> cacheMemberCard(MemberCardModel memberCard);
|
||||
|
||||
/// Get cached promotions
|
||||
///
|
||||
/// Returns list of cached [PromotionModel].
|
||||
/// Returns empty list if no cached data.
|
||||
/// Throws [CacheException] if data corrupted.
|
||||
Future<List<PromotionModel>> getPromotions();
|
||||
|
||||
/// Cache promotions
|
||||
///
|
||||
/// Stores list of [PromotionModel] in local database.
|
||||
/// Overwrites existing data.
|
||||
Future<void> cachePromotions(List<PromotionModel> promotions);
|
||||
|
||||
/// Clear all cached home data
|
||||
///
|
||||
/// Used when:
|
||||
/// - User logs out
|
||||
/// - Cache needs to be invalidated
|
||||
Future<void> clearCache();
|
||||
|
||||
/// Check if member card cache is valid
|
||||
///
|
||||
/// Returns true if cache exists and not expired.
|
||||
/// Cache is considered expired after 24 hours.
|
||||
Future<bool> isMemberCardCacheValid();
|
||||
|
||||
/// Check if promotions cache is valid
|
||||
///
|
||||
/// Returns true if cache exists and not expired.
|
||||
/// Cache is considered expired after 1 hour.
|
||||
Future<bool> isPromotionsCacheValid();
|
||||
}
|
||||
|
||||
/// Mock Implementation of Home Local Data Source
|
||||
///
|
||||
/// **TEMPORARY**: Uses hardcoded mock JSON data
|
||||
/// **TODO**: Replace with real Hive implementation when API is available
|
||||
///
|
||||
/// This mock implementation provides realistic data for development and testing.
|
||||
/// Uses the exact same interface as the real implementation will.
|
||||
class HomeLocalDataSourceImpl implements HomeLocalDataSource {
|
||||
/// Mock JSON data for member card
|
||||
static const Map<String, dynamic> _mockMemberCardJson = {
|
||||
'memberId': 'M001',
|
||||
'name': 'La Nguyen Quynh',
|
||||
'memberType': 'architect',
|
||||
'tier': 'diamond',
|
||||
'points': 9750,
|
||||
'validUntil': '2025-12-31T23:59:59.000Z',
|
||||
'qrData': '0983441099'
|
||||
};
|
||||
|
||||
/// Mock JSON data for promotions
|
||||
static const List<Map<String, dynamic>> _mockPromotionsJson = [
|
||||
{
|
||||
'id': 'P001',
|
||||
'title': 'Mua công nhắc - Khuyến mãi cảng lớn',
|
||||
'description': 'Giảm đến 30% cho đơn hàng từ 10 triệu',
|
||||
'imageUrl':
|
||||
'https://images.unsplash.com/photo-1615971677499-5467cbab01c0?w=280&h=140&fit=crop',
|
||||
'startDate': '2025-01-01T00:00:00.000Z',
|
||||
'endDate': '2025-12-31T23:59:59.000Z',
|
||||
'discountPercentage': 30,
|
||||
},
|
||||
{
|
||||
'id': 'P002',
|
||||
'title': 'Keo chà ron tặng kèm',
|
||||
'description': 'Mua gạch Eurotile tặng keo chà ron cao cấp',
|
||||
'imageUrl':
|
||||
'https://images.unsplash.com/photo-1542314831-068cd1dbfeeb?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=80',
|
||||
'startDate': '2025-01-01T00:00:00.000Z',
|
||||
'endDate': '2025-12-31T23:59:59.000Z',
|
||||
},
|
||||
{
|
||||
'id': 'P003',
|
||||
'title': 'Ưu đãi đặc biệt thành viên VIP',
|
||||
'description': 'Chiết khấu thêm 5% cho thành viên Diamond',
|
||||
'imageUrl':
|
||||
'https://images.unsplash.com/photo-1565538420870-da08ff96a207?w=280&h=140&fit=crop',
|
||||
'startDate': '2025-01-01T00:00:00.000Z',
|
||||
'endDate': '2025-12-31T23:59:59.000Z',
|
||||
'discountPercentage': 5,
|
||||
}
|
||||
];
|
||||
|
||||
/// Constructor
|
||||
const HomeLocalDataSourceImpl();
|
||||
|
||||
@override
|
||||
Future<MemberCardModel> getMemberCard() async {
|
||||
// Simulate network delay
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
|
||||
try {
|
||||
// Parse mock JSON data
|
||||
return MemberCardModel.fromJson(_mockMemberCardJson);
|
||||
} catch (e) {
|
||||
throw CacheException('Failed to get cached member card: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> cacheMemberCard(MemberCardModel memberCard) async {
|
||||
// Simulate write delay
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
// Mock implementation - does nothing
|
||||
// TODO: Implement Hive write logic when API is available
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<PromotionModel>> getPromotions() async {
|
||||
// Simulate network delay
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
try {
|
||||
// Parse mock JSON data
|
||||
return _mockPromotionsJson
|
||||
.map((json) => PromotionModel.fromJson(json))
|
||||
.toList();
|
||||
} catch (e) {
|
||||
throw CacheException('Failed to get cached promotions: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> cachePromotions(List<PromotionModel> promotions) async {
|
||||
// Simulate write delay
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
// Mock implementation - does nothing
|
||||
// TODO: Implement Hive write logic when API is available
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> clearCache() async {
|
||||
// Simulate operation delay
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
// Mock implementation - does nothing
|
||||
// TODO: Implement cache clearing when API is available
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> isMemberCardCacheValid() async {
|
||||
// Mock implementation - always return true for development
|
||||
// TODO: Implement real cache validation when API is available
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> isPromotionsCacheValid() async {
|
||||
// Mock implementation - always return true for development
|
||||
// TODO: Implement real cache validation when API is available
|
||||
return true;
|
||||
}
|
||||
}
|
||||
184
lib/features/home/data/models/member_card_model.dart
Normal file
184
lib/features/home/data/models/member_card_model.dart
Normal file
@@ -0,0 +1,184 @@
|
||||
/// Data Model: Member Card
|
||||
///
|
||||
/// Data Transfer Object for member card information.
|
||||
/// This model handles serialization/deserialization for:
|
||||
/// - JSON (API responses)
|
||||
/// - Hive (local database)
|
||||
///
|
||||
/// Extends the domain entity and adds data layer functionality.
|
||||
library;
|
||||
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import 'package:worker/features/home/domain/entities/member_card.dart';
|
||||
|
||||
part 'member_card_model.g.dart';
|
||||
|
||||
/// Member Card Model
|
||||
///
|
||||
/// Used for:
|
||||
/// - API JSON serialization/deserialization
|
||||
/// - Hive local database storage
|
||||
/// - Converting to/from domain entity
|
||||
///
|
||||
/// Hive Type ID: 10 (ensure this doesn't conflict with other models)
|
||||
@HiveType(typeId: 10)
|
||||
class MemberCardModel extends HiveObject {
|
||||
/// Member ID
|
||||
@HiveField(0)
|
||||
final String memberId;
|
||||
|
||||
/// Member name
|
||||
@HiveField(1)
|
||||
final String name;
|
||||
|
||||
/// Member type (stored as string for serialization)
|
||||
@HiveField(2)
|
||||
final String memberType;
|
||||
|
||||
/// Membership tier (stored as string for serialization)
|
||||
@HiveField(3)
|
||||
final String tier;
|
||||
|
||||
/// Current points
|
||||
@HiveField(4)
|
||||
final int points;
|
||||
|
||||
/// Card expiration date (stored as ISO8601 string)
|
||||
@HiveField(5)
|
||||
final String validUntil;
|
||||
|
||||
/// QR code data
|
||||
@HiveField(6)
|
||||
final String qrData;
|
||||
|
||||
MemberCardModel({
|
||||
required this.memberId,
|
||||
required this.name,
|
||||
required this.memberType,
|
||||
required this.tier,
|
||||
required this.points,
|
||||
required this.validUntil,
|
||||
required this.qrData,
|
||||
});
|
||||
|
||||
/// From JSON constructor
|
||||
factory MemberCardModel.fromJson(Map<String, dynamic> json) {
|
||||
return MemberCardModel(
|
||||
memberId: json['memberId'] as String,
|
||||
name: json['name'] as String,
|
||||
memberType: json['memberType'] as String,
|
||||
tier: json['tier'] as String,
|
||||
points: json['points'] as int,
|
||||
validUntil: json['validUntil'] as String,
|
||||
qrData: json['qrData'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
/// To JSON method
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'memberId': memberId,
|
||||
'name': name,
|
||||
'memberType': memberType,
|
||||
'tier': tier,
|
||||
'points': points,
|
||||
'validUntil': validUntil,
|
||||
'qrData': qrData,
|
||||
};
|
||||
}
|
||||
|
||||
/// Convert to domain entity
|
||||
MemberCard toEntity() {
|
||||
return MemberCard(
|
||||
memberId: memberId,
|
||||
name: name,
|
||||
memberType: _parseMemberType(memberType),
|
||||
tier: _parseMemberTier(tier),
|
||||
points: points,
|
||||
validUntil: DateTime.parse(validUntil),
|
||||
qrData: qrData,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create from domain entity
|
||||
factory MemberCardModel.fromEntity(MemberCard entity) {
|
||||
return MemberCardModel(
|
||||
memberId: entity.memberId,
|
||||
name: entity.name,
|
||||
memberType: entity.memberType.name,
|
||||
tier: entity.tier.name,
|
||||
points: entity.points,
|
||||
validUntil: entity.validUntil.toIso8601String(),
|
||||
qrData: entity.qrData,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parse member type from string
|
||||
static MemberType _parseMemberType(String type) {
|
||||
return MemberType.values.firstWhere(
|
||||
(e) => e.name.toLowerCase() == type.toLowerCase(),
|
||||
orElse: () => MemberType.contractor, // Default fallback
|
||||
);
|
||||
}
|
||||
|
||||
/// Parse member tier from string
|
||||
static MemberTier _parseMemberTier(String tier) {
|
||||
return MemberTier.values.firstWhere(
|
||||
(e) => e.name.toLowerCase() == tier.toLowerCase(),
|
||||
orElse: () => MemberTier.gold, // Default fallback
|
||||
);
|
||||
}
|
||||
|
||||
/// Copy with method for creating modified copies
|
||||
MemberCardModel copyWith({
|
||||
String? memberId,
|
||||
String? name,
|
||||
String? memberType,
|
||||
String? tier,
|
||||
int? points,
|
||||
String? validUntil,
|
||||
String? qrData,
|
||||
}) {
|
||||
return MemberCardModel(
|
||||
memberId: memberId ?? this.memberId,
|
||||
name: name ?? this.name,
|
||||
memberType: memberType ?? this.memberType,
|
||||
tier: tier ?? this.tier,
|
||||
points: points ?? this.points,
|
||||
validUntil: validUntil ?? this.validUntil,
|
||||
qrData: qrData ?? this.qrData,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'MemberCardModel(memberId: $memberId, name: $name, memberType: $memberType, tier: $tier, points: $points)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is MemberCardModel &&
|
||||
other.memberId == memberId &&
|
||||
other.name == name &&
|
||||
other.memberType == memberType &&
|
||||
other.tier == tier &&
|
||||
other.points == points &&
|
||||
other.validUntil == validUntil &&
|
||||
other.qrData == qrData;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return Object.hash(
|
||||
memberId,
|
||||
name,
|
||||
memberType,
|
||||
tier,
|
||||
points,
|
||||
validUntil,
|
||||
qrData,
|
||||
);
|
||||
}
|
||||
}
|
||||
59
lib/features/home/data/models/member_card_model.g.dart
Normal file
59
lib/features/home/data/models/member_card_model.g.dart
Normal file
@@ -0,0 +1,59 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'member_card_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class MemberCardModelAdapter extends TypeAdapter<MemberCardModel> {
|
||||
@override
|
||||
final typeId = 10;
|
||||
|
||||
@override
|
||||
MemberCardModel read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return MemberCardModel(
|
||||
memberId: fields[0] as String,
|
||||
name: fields[1] as String,
|
||||
memberType: fields[2] as String,
|
||||
tier: fields[3] as String,
|
||||
points: (fields[4] as num).toInt(),
|
||||
validUntil: fields[5] as String,
|
||||
qrData: fields[6] as String,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, MemberCardModel obj) {
|
||||
writer
|
||||
..writeByte(7)
|
||||
..writeByte(0)
|
||||
..write(obj.memberId)
|
||||
..writeByte(1)
|
||||
..write(obj.name)
|
||||
..writeByte(2)
|
||||
..write(obj.memberType)
|
||||
..writeByte(3)
|
||||
..write(obj.tier)
|
||||
..writeByte(4)
|
||||
..write(obj.points)
|
||||
..writeByte(5)
|
||||
..write(obj.validUntil)
|
||||
..writeByte(6)
|
||||
..write(obj.qrData);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is MemberCardModelAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
207
lib/features/home/data/models/promotion_model.dart
Normal file
207
lib/features/home/data/models/promotion_model.dart
Normal file
@@ -0,0 +1,207 @@
|
||||
/// Data Model: Promotion
|
||||
///
|
||||
/// Data Transfer Object for promotion information.
|
||||
/// This model handles serialization/deserialization for:
|
||||
/// - JSON (API responses)
|
||||
/// - Hive (local database)
|
||||
///
|
||||
/// Extends the domain entity and adds data layer functionality.
|
||||
library;
|
||||
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import 'package:worker/features/home/domain/entities/promotion.dart';
|
||||
|
||||
part 'promotion_model.g.dart';
|
||||
|
||||
/// Promotion Model
|
||||
///
|
||||
/// Used for:
|
||||
/// - API JSON serialization/deserialization
|
||||
/// - Hive local database storage
|
||||
/// - Converting to/from domain entity
|
||||
///
|
||||
/// Hive Type ID: 11 (ensure this doesn't conflict with other models)
|
||||
@HiveType(typeId: 11)
|
||||
class PromotionModel extends HiveObject {
|
||||
/// Promotion ID
|
||||
@HiveField(0)
|
||||
final String id;
|
||||
|
||||
/// Promotion title
|
||||
@HiveField(1)
|
||||
final String title;
|
||||
|
||||
/// Description
|
||||
@HiveField(2)
|
||||
final String description;
|
||||
|
||||
/// Image URL
|
||||
@HiveField(3)
|
||||
final String imageUrl;
|
||||
|
||||
/// Start date (ISO8601 string)
|
||||
@HiveField(4)
|
||||
final String startDate;
|
||||
|
||||
/// End date (ISO8601 string)
|
||||
@HiveField(5)
|
||||
final String endDate;
|
||||
|
||||
/// Discount percentage (nullable)
|
||||
@HiveField(6)
|
||||
final int? discountPercentage;
|
||||
|
||||
/// Discount amount (nullable)
|
||||
@HiveField(7)
|
||||
final double? discountAmount;
|
||||
|
||||
/// Terms and conditions (nullable)
|
||||
@HiveField(8)
|
||||
final String? terms;
|
||||
|
||||
/// Details URL (nullable)
|
||||
@HiveField(9)
|
||||
final String? detailsUrl;
|
||||
|
||||
PromotionModel({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.imageUrl,
|
||||
required this.startDate,
|
||||
required this.endDate,
|
||||
this.discountPercentage,
|
||||
this.discountAmount,
|
||||
this.terms,
|
||||
this.detailsUrl,
|
||||
});
|
||||
|
||||
/// From JSON constructor
|
||||
factory PromotionModel.fromJson(Map<String, dynamic> json) {
|
||||
return PromotionModel(
|
||||
id: json['id'] as String,
|
||||
title: json['title'] as String,
|
||||
description: json['description'] as String,
|
||||
imageUrl: json['imageUrl'] as String,
|
||||
startDate: json['startDate'] as String,
|
||||
endDate: json['endDate'] as String,
|
||||
discountPercentage: json['discountPercentage'] as int?,
|
||||
discountAmount: json['discountAmount'] as double?,
|
||||
terms: json['terms'] as String?,
|
||||
detailsUrl: json['detailsUrl'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
/// To JSON method
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'title': title,
|
||||
'description': description,
|
||||
'imageUrl': imageUrl,
|
||||
'startDate': startDate,
|
||||
'endDate': endDate,
|
||||
'discountPercentage': discountPercentage,
|
||||
'discountAmount': discountAmount,
|
||||
'terms': terms,
|
||||
'detailsUrl': detailsUrl,
|
||||
};
|
||||
}
|
||||
|
||||
/// Convert to domain entity
|
||||
Promotion toEntity() {
|
||||
return Promotion(
|
||||
id: id,
|
||||
title: title,
|
||||
description: description,
|
||||
imageUrl: imageUrl,
|
||||
startDate: DateTime.parse(startDate),
|
||||
endDate: DateTime.parse(endDate),
|
||||
discountPercentage: discountPercentage,
|
||||
discountAmount: discountAmount,
|
||||
terms: terms,
|
||||
detailsUrl: detailsUrl,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create from domain entity
|
||||
factory PromotionModel.fromEntity(Promotion entity) {
|
||||
return PromotionModel(
|
||||
id: entity.id,
|
||||
title: entity.title,
|
||||
description: entity.description,
|
||||
imageUrl: entity.imageUrl,
|
||||
startDate: entity.startDate.toIso8601String(),
|
||||
endDate: entity.endDate.toIso8601String(),
|
||||
discountPercentage: entity.discountPercentage,
|
||||
discountAmount: entity.discountAmount,
|
||||
terms: entity.terms,
|
||||
detailsUrl: entity.detailsUrl,
|
||||
);
|
||||
}
|
||||
|
||||
/// Copy with method for creating modified copies
|
||||
PromotionModel copyWith({
|
||||
String? id,
|
||||
String? title,
|
||||
String? description,
|
||||
String? imageUrl,
|
||||
String? startDate,
|
||||
String? endDate,
|
||||
int? discountPercentage,
|
||||
double? discountAmount,
|
||||
String? terms,
|
||||
String? detailsUrl,
|
||||
}) {
|
||||
return PromotionModel(
|
||||
id: id ?? this.id,
|
||||
title: title ?? this.title,
|
||||
description: description ?? this.description,
|
||||
imageUrl: imageUrl ?? this.imageUrl,
|
||||
startDate: startDate ?? this.startDate,
|
||||
endDate: endDate ?? this.endDate,
|
||||
discountPercentage: discountPercentage ?? this.discountPercentage,
|
||||
discountAmount: discountAmount ?? this.discountAmount,
|
||||
terms: terms ?? this.terms,
|
||||
detailsUrl: detailsUrl ?? this.detailsUrl,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PromotionModel(id: $id, title: $title, startDate: $startDate, endDate: $endDate)';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is PromotionModel &&
|
||||
other.id == id &&
|
||||
other.title == title &&
|
||||
other.description == description &&
|
||||
other.imageUrl == imageUrl &&
|
||||
other.startDate == startDate &&
|
||||
other.endDate == endDate &&
|
||||
other.discountPercentage == discountPercentage &&
|
||||
other.discountAmount == discountAmount &&
|
||||
other.terms == terms &&
|
||||
other.detailsUrl == detailsUrl;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return Object.hash(
|
||||
id,
|
||||
title,
|
||||
description,
|
||||
imageUrl,
|
||||
startDate,
|
||||
endDate,
|
||||
discountPercentage,
|
||||
discountAmount,
|
||||
terms,
|
||||
detailsUrl,
|
||||
);
|
||||
}
|
||||
}
|
||||
68
lib/features/home/data/models/promotion_model.g.dart
Normal file
68
lib/features/home/data/models/promotion_model.g.dart
Normal file
@@ -0,0 +1,68 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'promotion_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class PromotionModelAdapter extends TypeAdapter<PromotionModel> {
|
||||
@override
|
||||
final typeId = 11;
|
||||
|
||||
@override
|
||||
PromotionModel read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return PromotionModel(
|
||||
id: fields[0] as String,
|
||||
title: fields[1] as String,
|
||||
description: fields[2] as String,
|
||||
imageUrl: fields[3] as String,
|
||||
startDate: fields[4] as String,
|
||||
endDate: fields[5] as String,
|
||||
discountPercentage: (fields[6] as num?)?.toInt(),
|
||||
discountAmount: (fields[7] as num?)?.toDouble(),
|
||||
terms: fields[8] as String?,
|
||||
detailsUrl: fields[9] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, PromotionModel obj) {
|
||||
writer
|
||||
..writeByte(10)
|
||||
..writeByte(0)
|
||||
..write(obj.id)
|
||||
..writeByte(1)
|
||||
..write(obj.title)
|
||||
..writeByte(2)
|
||||
..write(obj.description)
|
||||
..writeByte(3)
|
||||
..write(obj.imageUrl)
|
||||
..writeByte(4)
|
||||
..write(obj.startDate)
|
||||
..writeByte(5)
|
||||
..write(obj.endDate)
|
||||
..writeByte(6)
|
||||
..write(obj.discountPercentage)
|
||||
..writeByte(7)
|
||||
..write(obj.discountAmount)
|
||||
..writeByte(8)
|
||||
..write(obj.terms)
|
||||
..writeByte(9)
|
||||
..write(obj.detailsUrl);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is PromotionModelAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
161
lib/features/home/data/repositories/home_repository_impl.dart
Normal file
161
lib/features/home/data/repositories/home_repository_impl.dart
Normal file
@@ -0,0 +1,161 @@
|
||||
/// Repository Implementation: Home Repository
|
||||
///
|
||||
/// Concrete implementation of the HomeRepository interface.
|
||||
/// Coordinates between local and remote data sources to provide home data.
|
||||
///
|
||||
/// Implements offline-first strategy:
|
||||
/// 1. Try to return cached data immediately
|
||||
/// 2. Fetch fresh data from server in background
|
||||
/// 3. Update cache with fresh data
|
||||
library;
|
||||
|
||||
import 'package:worker/core/errors/exceptions.dart';
|
||||
import 'package:worker/core/errors/failures.dart';
|
||||
import 'package:worker/features/home/data/datasources/home_local_datasource.dart';
|
||||
import 'package:worker/features/home/domain/entities/member_card.dart';
|
||||
import 'package:worker/features/home/domain/entities/promotion.dart';
|
||||
import 'package:worker/features/home/domain/repositories/home_repository.dart';
|
||||
|
||||
/// Home Repository Implementation
|
||||
///
|
||||
/// Responsibilities:
|
||||
/// - Coordinate between local cache and remote API
|
||||
/// - Implement offline-first data strategy
|
||||
/// - Handle errors and convert to domain failures
|
||||
/// - Manage cache invalidation
|
||||
class HomeRepositoryImpl implements HomeRepository {
|
||||
/// Local data source (Hive)
|
||||
final HomeLocalDataSource localDataSource;
|
||||
|
||||
/// Remote data source (API) - TODO: Add when API is ready
|
||||
// final HomeRemoteDataSource remoteDataSource;
|
||||
|
||||
/// Constructor
|
||||
HomeRepositoryImpl({
|
||||
required this.localDataSource,
|
||||
// required this.remoteDataSource, // TODO: Add when API ready
|
||||
});
|
||||
|
||||
@override
|
||||
Future<MemberCard> getMemberCard() async {
|
||||
try {
|
||||
// TODO: Implement offline-first strategy
|
||||
// 1. Check if cache is valid
|
||||
final isCacheValid = await localDataSource.isMemberCardCacheValid();
|
||||
|
||||
if (isCacheValid) {
|
||||
// 2. Return cached data if valid
|
||||
final cachedModel = await localDataSource.getMemberCard();
|
||||
return cachedModel.toEntity();
|
||||
}
|
||||
|
||||
// 3. If cache invalid, fetch from remote (when API ready)
|
||||
// For now, try to return cached data even if expired
|
||||
try {
|
||||
final cachedModel = await localDataSource.getMemberCard();
|
||||
return cachedModel.toEntity();
|
||||
} catch (e) {
|
||||
// TODO: Fetch from remote API when available
|
||||
// final remoteModel = await remoteDataSource.getMemberCard();
|
||||
// await localDataSource.cacheMemberCard(remoteModel);
|
||||
// return remoteModel.toEntity();
|
||||
|
||||
throw const CacheException('No member card data available');
|
||||
}
|
||||
} on CacheException catch (e) {
|
||||
throw CacheFailure(message: e.message);
|
||||
} catch (e) {
|
||||
throw ServerFailure(message: 'Failed to get member card: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Promotion>> getPromotions() async {
|
||||
try {
|
||||
// TODO: Implement offline-first strategy
|
||||
// 1. Check if cache is valid
|
||||
final isCacheValid = await localDataSource.isPromotionsCacheValid();
|
||||
|
||||
if (isCacheValid) {
|
||||
// 2. Return cached data if valid
|
||||
final cachedModels = await localDataSource.getPromotions();
|
||||
return cachedModels.map((model) => model.toEntity()).toList();
|
||||
}
|
||||
|
||||
// 3. If cache invalid, fetch from remote (when API ready)
|
||||
// For now, try to return cached data even if expired
|
||||
try {
|
||||
final cachedModels = await localDataSource.getPromotions();
|
||||
return cachedModels.map((model) => model.toEntity()).toList();
|
||||
} catch (e) {
|
||||
// TODO: Fetch from remote API when available
|
||||
// final remoteModels = await remoteDataSource.getPromotions();
|
||||
// await localDataSource.cachePromotions(remoteModels);
|
||||
// return remoteModels.map((m) => m.toEntity()).toList();
|
||||
|
||||
// Return empty list if no data available
|
||||
return [];
|
||||
}
|
||||
} on CacheException catch (e) {
|
||||
throw CacheFailure(message: e.message);
|
||||
} catch (e) {
|
||||
throw ServerFailure(message: 'Failed to get promotions: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<MemberCard> refreshMemberCard() async {
|
||||
try {
|
||||
// TODO: Implement force refresh from API
|
||||
// This should always fetch from remote, bypassing cache
|
||||
|
||||
// When API is ready:
|
||||
// 1. Fetch from remote
|
||||
// final remoteModel = await remoteDataSource.getMemberCard();
|
||||
|
||||
// 2. Update cache
|
||||
// await localDataSource.cacheMemberCard(remoteModel);
|
||||
|
||||
// 3. Return entity
|
||||
// return remoteModel.toEntity();
|
||||
|
||||
// For now, just return cached data
|
||||
final cachedModel = await localDataSource.getMemberCard();
|
||||
return cachedModel.toEntity();
|
||||
} on ServerException catch (e) {
|
||||
throw ServerFailure(message: e.message);
|
||||
} on NetworkException catch (e) {
|
||||
throw NetworkFailure(message: e.message);
|
||||
} catch (e) {
|
||||
throw ServerFailure(message: 'Failed to refresh member card: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Promotion>> refreshPromotions() async {
|
||||
try {
|
||||
// TODO: Implement force refresh from API
|
||||
// This should always fetch from remote, bypassing cache
|
||||
|
||||
// When API is ready:
|
||||
// 1. Fetch from remote
|
||||
// final remoteModels = await remoteDataSource.getPromotions();
|
||||
|
||||
// 2. Update cache
|
||||
// await localDataSource.cachePromotions(remoteModels);
|
||||
|
||||
// 3. Return entities
|
||||
// return remoteModels.map((m) => m.toEntity()).toList();
|
||||
|
||||
// For now, just return cached data
|
||||
final cachedModels = await localDataSource.getPromotions();
|
||||
return cachedModels.map((model) => model.toEntity()).toList();
|
||||
} on ServerException catch (e) {
|
||||
throw ServerFailure(message: e.message);
|
||||
} on NetworkException catch (e) {
|
||||
throw NetworkFailure(message: e.message);
|
||||
} catch (e) {
|
||||
throw ServerFailure(message: 'Failed to refresh promotions: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user