category repo

This commit is contained in:
efrilm 2025-10-24 20:06:42 +07:00
parent 683fff6eeb
commit 71fa4823fc
29 changed files with 5434 additions and 30 deletions

View File

@ -0,0 +1,312 @@
import 'dart:async';
import 'dart:developer';
import 'package:bloc/bloc.dart';
import 'package:dartz/dartz.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../../domain/category/category.dart';
part 'category_loader_event.dart';
part 'category_loader_state.dart';
part 'category_loader_bloc.freezed.dart';
class CategoryLoaderBloc
extends Bloc<CategoryLoaderEvent, CategoryLoaderState> {
final ICategoryRepository _categoryRepository;
Timer? _searchDebounce;
bool _isLoadingMore = false;
CategoryLoaderBloc(this._categoryRepository)
: super(CategoryLoaderState.initial()) {
on<CategoryLoaderEvent>(_onCategoryLoaderEvent);
}
Future<void> _onCategoryLoaderEvent(
CategoryLoaderEvent event,
Emitter<CategoryLoaderState> emit,
) {
return event.map(
getCategories: (e) async {
emit(state.copyWith(isLoadingMore: true));
log(
'📱 Loading categories - isActive: ${e.isActive}, forceRemote: ${e.forceRemote}',
);
final result = await _categoryRepository.getCategories(
page: 1,
limit: 50,
isActive: e.isActive,
search: e.search,
forceRemote: e.forceRemote,
);
await result.fold(
(failure) async {
emit(
state.copyWith(
isLoadingMore: false,
failureOptionCategory: optionOf(failure),
),
);
},
(response) async {
final categories = [Category.all(), ...response.categories];
final totalPages = response.totalPages;
final hasReachedMax = categories.length < 50 || 1 >= totalPages;
log(
'✅ Categories loaded: ${categories.length}, hasReachedMax: $hasReachedMax',
);
emit(
state.copyWith(
categories: categories,
page: 1,
hasReachedMax: hasReachedMax,
isLoadingMore: false,
failureOptionCategory: none(),
),
);
},
);
},
loadMore: (e) async {
final currentState = state;
// HAPUS pengecekan is! _Loaded karena state cuma 1 class doang
if (currentState.hasReachedMax ||
_isLoadingMore ||
currentState.isLoadingMore) {
log(
'⏹️ Load more blocked - hasReachedMax: ${currentState.hasReachedMax}, isLoadingMore: $_isLoadingMore',
);
return;
}
_isLoadingMore = true;
emit(currentState.copyWith(isLoadingMore: true));
final nextPage = currentState.page + 1; // Ganti currentPage jadi page
log('📄 Loading more categories - page: $nextPage');
try {
final result = await _categoryRepository.getCategories(
page: nextPage,
limit: 10,
isActive: true,
search: currentState.searchQuery,
);
await result.fold(
(failure) async {
log('❌ Error loading more categories: $failure');
emit(currentState.copyWith(isLoadingMore: false));
},
(response) async {
final newCategories = response.categories;
final totalPages = response.totalPages;
// Prevent duplicate categories
final currentCategoryIds = currentState.categories
.map((c) => c.id)
.toSet();
final filteredNewCategories = newCategories
.where(
(category) => !currentCategoryIds.contains(category.id),
)
.toList();
final allCategories = List<Category>.from(currentState.categories)
..addAll(filteredNewCategories);
final hasReachedMax =
newCategories.length < 10 || nextPage >= totalPages;
log(
'✅ More categories loaded: ${filteredNewCategories.length} new, total: ${allCategories.length}',
);
emit(
currentState.copyWith(
categories: allCategories,
hasReachedMax: hasReachedMax,
page: nextPage, // Update page
isLoadingMore: false,
),
);
},
);
} catch (e) {
log('❌ Exception loading more categories: $e');
emit(currentState.copyWith(isLoadingMore: false));
} finally {
_isLoadingMore = false;
}
},
refresh: (e) async {
final currentState = state;
bool isActive = true;
String? searchQuery = currentState.searchQuery;
_isLoadingMore = false;
_searchDebounce?.cancel();
log('🔄 Refreshing categories');
// Clear local cache
_categoryRepository.clearCache();
add(
CategoryLoaderEvent.getCategories(
isActive: isActive,
search: searchQuery,
forceRemote: true, // Force remote refresh
),
);
},
search: (e) async {
// Cancel previous search
_searchDebounce?.cancel();
// Debounce search for better UX
_searchDebounce = Timer(Duration(milliseconds: 300), () async {
emit(state.copyWith(isLoadingMore: true));
_isLoadingMore = false;
log('🔍 Searching categories: "${e.query}"');
final result = await _categoryRepository.getCategories(
page: 1,
limit: 20, // More results for search
isActive: e.isActive,
search: e.query,
);
await result.fold(
(failure) async {
log('❌ Search error: $failure');
emit(
state.copyWith(
isLoadingMore: false,
failureOptionCategory: optionOf(failure),
),
);
},
(response) async {
final categories = [Category.all(), ...response.categories];
final totalPages = response.totalPages;
final hasReachedMax = categories.length < 20 || 1 >= totalPages;
log('✅ Search results: ${categories.length} categories found');
emit(
state.copyWith(
categories: categories,
hasReachedMax: hasReachedMax,
page: 1,
isLoadingMore: false,
failureOptionCategory: none(),
searchQuery: e.query,
),
);
},
);
});
},
syncAll: (e) async {
emit(state.copyWith(isLoadingMore: true));
log('🔄 Starting full category sync...');
final result = await _categoryRepository.syncAllCategories();
await result.fold(
(failure) async {
log('❌ Sync failed: $failure');
emit(
state.copyWith(
isLoadingMore: false,
failureOptionCategory: optionOf(failure),
),
);
// After sync error, try to load local data
Timer(Duration(seconds: 2), () {
add(const CategoryLoaderEvent.getCategories());
});
},
(successMessage) async {
log('✅ Sync completed: $successMessage');
emit(
state.copyWith(
isLoadingMore: false,
failureOptionCategory: none(),
),
);
// After successful sync, load the updated data
Timer(Duration(seconds: 1), () {
add(const CategoryLoaderEvent.getCategories());
});
},
);
},
getAllCategories: (e) async {
try {
log('📋 Loading all categories for dropdown...');
// final categories = await _categoryRepository.getAllCategories();
// emit(
// state.copyWith(
// categories: categories,
// isLoadingMore: false,
// failureOptionCategory: none(),
// ),
// );
// log('✅ All categories loaded: ${categories.length}');
} catch (e) {
log('❌ Error loading all categories: $e');
emit(
state.copyWith(
isLoadingMore: false,
failureOptionCategory: optionOf(
CategoryFailure.dynamicErrorMessage(
'Gagal memuat semua kategori: $e',
),
),
),
);
}
},
getDatabaseStats: (e) async {
try {
final stats = await _categoryRepository.getDatabaseStats();
log('📊 Category database stats retrieved: $stats');
// You can emit a special state here if needed for UI updates
// For now, just log the stats
} catch (e) {
log('❌ Error getting category database stats: $e');
}
},
clearCache: (e) async {
log('🧹 Manually clearing category cache');
_categoryRepository.clearCache();
// Refresh current data after cache clear
add(const CategoryLoaderEvent.refresh());
},
);
}
@override
Future<void> close() {
_searchDebounce?.cancel();
return super.close();
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,27 @@
part of 'category_loader_bloc.dart';
@freezed
class CategoryLoaderEvent with _$CategoryLoaderEvent {
const factory CategoryLoaderEvent.getCategories({
@Default(true) bool isActive,
String? search,
@Default(false) bool forceRemote,
}) = _GetCategories;
const factory CategoryLoaderEvent.loadMore() = _LoadMore;
const factory CategoryLoaderEvent.refresh() = _Refresh;
const factory CategoryLoaderEvent.search({
required String query,
@Default(true) bool isActive,
}) = _Search;
const factory CategoryLoaderEvent.syncAll() = _SyncAll;
const factory CategoryLoaderEvent.getAllCategories() = _GetAllCategories;
const factory CategoryLoaderEvent.getDatabaseStats() = _GetDatabaseStats;
const factory CategoryLoaderEvent.clearCache() = _ClearCache;
}

View File

@ -0,0 +1,16 @@
part of 'category_loader_bloc.dart';
@freezed
class CategoryLoaderState with _$CategoryLoaderState {
factory CategoryLoaderState({
required List<Category> categories,
required Option<CategoryFailure> failureOptionCategory,
@Default(false) bool hasReachedMax,
@Default(1) int page,
@Default(false) bool isLoadingMore,
String? searchQuery,
}) = _CategoryLoaderState;
factory CategoryLoaderState.initial() =>
CategoryLoaderState(categories: [], failureOptionCategory: none());
}

View File

@ -1,3 +1,5 @@
class AppConstant { class AppConstant {
static const String appName = "Apskel POS"; static const String appName = "Apskel POS";
static const int cacheExpire = 10; // in minutes
} }

View File

@ -0,0 +1,158 @@
import 'dart:async';
import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';
class DatabaseHelper {
static Database? _database;
Future<Database> get database async {
_database ??= await _initDatabase();
return _database!;
}
Future<Database> _initDatabase() async {
String path = join(await getDatabasesPath(), 'db_pos.db');
return await openDatabase(
path,
version: 1, // Updated version for categories table
onCreate: _onCreate,
onUpgrade: _onUpgrade,
);
}
Future<void> _onCreate(Database db, int version) async {
// Products table
await db.execute('''
CREATE TABLE products (
id TEXT PRIMARY KEY,
organization_id TEXT,
category_id TEXT,
sku TEXT,
name TEXT,
description TEXT,
price INTEGER,
cost INTEGER,
business_type TEXT,
image_url TEXT,
printer_type TEXT,
metadata TEXT,
is_active INTEGER,
created_at TEXT,
updated_at TEXT
)
''');
// Product Variants table
await db.execute('''
CREATE TABLE product_variants (
id TEXT PRIMARY KEY,
product_id TEXT,
name TEXT,
price_modifier INTEGER,
cost INTEGER,
metadata TEXT,
created_at TEXT,
updated_at TEXT,
FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE CASCADE
)
''');
// Categories table - NEW
await db.execute('''
CREATE TABLE categories (
id TEXT PRIMARY KEY,
organization_id TEXT,
name TEXT NOT NULL,
description TEXT,
business_type TEXT,
metadata TEXT,
is_active INTEGER DEFAULT 1,
created_at TEXT,
updated_at TEXT
)
''');
// Printer table
await db.execute('''
CREATE TABLE printers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
address TEXT,
paper TEXT,
type TEXT,
created_at TEXT,
updated_at TEXT
)
''');
// Create indexes for better performance
await db.execute(
'CREATE INDEX idx_products_category_id ON products(category_id)',
);
await db.execute('CREATE INDEX idx_products_name ON products(name)');
await db.execute('CREATE INDEX idx_products_sku ON products(sku)');
await db.execute('CREATE INDEX idx_categories_name ON categories(name)');
await db.execute(
'CREATE INDEX idx_categories_organization_id ON categories(organization_id)',
);
await db.execute(
'CREATE INDEX idx_categories_is_active ON categories(is_active)',
);
await db.execute('CREATE INDEX idx_printers_code ON printers(code)');
await db.execute('CREATE INDEX idx_printers_type ON printers(type)');
}
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
if (oldVersion < 2) {
// Add printer table in version 2
await db.execute('''
CREATE TABLE printers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
address TEXT,
paper TEXT,
type TEXT,
created_at TEXT,
updated_at TEXT
)
''');
await db.execute('CREATE INDEX idx_printers_code ON printers(code)');
await db.execute('CREATE INDEX idx_printers_type ON printers(type)');
}
if (oldVersion < 3) {
// Add categories table in version 3
await db.execute('''
CREATE TABLE categories (
id TEXT PRIMARY KEY,
organization_id TEXT,
name TEXT NOT NULL,
description TEXT,
business_type TEXT,
metadata TEXT,
is_active INTEGER DEFAULT 1,
created_at TEXT,
updated_at TEXT
)
''');
await db.execute('CREATE INDEX idx_categories_name ON categories(name)');
await db.execute(
'CREATE INDEX idx_categories_organization_id ON categories(organization_id)',
);
await db.execute(
'CREATE INDEX idx_categories_is_active ON categories(is_active)',
);
}
}
Future<void> close() async {
final db = await database;
await db.close();
_database = null;
}
}

View File

@ -0,0 +1,9 @@
import 'package:injectable/injectable.dart';
import '../database/database_helper.dart';
@module
abstract class DatabaseDi {
@singleton
DatabaseHelper get databaseHelper => DatabaseHelper();
}

View File

@ -1,4 +1,5 @@
class ApiPath { class ApiPath {
static const String login = '/api/v1/auth/login'; static const String login = '/api/v1/auth/login';
static const String outlets = '/api/v1/outlets'; static const String outlets = '/api/v1/outlets';
static const String categories = '/api/v1/categories';
} }

View File

@ -0,0 +1,10 @@
import 'package:dartz/dartz.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../common/api/api_failure.dart';
part 'category.freezed.dart';
part 'entities/category_entity.dart';
part 'failures/category_failure.dart';
part 'repositories/i_category_repository.dart';

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,59 @@
part of '../category.dart';
@freezed
class ListCategory with _$ListCategory {
const factory ListCategory({
required List<Category> categories,
required int totalCount,
required int page,
required int limit,
required int totalPages,
}) = _ListCategory;
factory ListCategory.empty() => const ListCategory(
categories: [],
totalCount: 0,
page: 0,
limit: 0,
totalPages: 0,
);
}
@freezed
class Category with _$Category {
const factory Category({
required String id,
required String organizationId,
required String name,
required String description,
required String businessType,
required int order,
required Map<String, dynamic> metadata,
required String createdAt,
required String updatedAt,
}) = _Category;
factory Category.empty() => const Category(
id: '',
organizationId: '',
name: '',
description: '',
businessType: '',
order: 0,
metadata: {},
createdAt: '',
updatedAt: '',
);
factory Category.all() => const Category(
id: 'all',
organizationId: '',
name: 'Semua',
businessType: 'restaurant',
metadata: {},
createdAt: '',
updatedAt: '',
description: '',
order: 1,
);
}

View File

@ -0,0 +1,12 @@
part of '../category.dart';
@freezed
sealed class CategoryFailure with _$CategoryFailure {
const factory CategoryFailure.serverError(ApiFailure failure) = _ServerError;
const factory CategoryFailure.unexpectedError() = _UnexpectedError;
const factory CategoryFailure.empty() = _Empty;
const factory CategoryFailure.localStorageError(String erroMessage) =
_LocalStorageError;
const factory CategoryFailure.dynamicErrorMessage(String erroMessage) =
_DynamicErrorMessage;
}

View File

@ -0,0 +1,26 @@
part of '../category.dart';
abstract class ICategoryRepository {
Future<Either<CategoryFailure, ListCategory>> getCategories({
int page = 1,
int limit = 10,
bool isActive = true,
String? search,
bool forceRemote = false,
});
Future<Either<CategoryFailure, Category>> getCategoryById(String id);
Future<Either<CategoryFailure, String>> syncAllCategories();
Future<Either<CategoryFailure, ListCategory>> refreshCategories({
bool isActive = true,
String? search,
});
Future<bool> hasLocalCategories();
Future<Either<CategoryFailure, Map<String, dynamic>>> getDatabaseStats();
void clearCache();
}

View File

@ -0,0 +1,10 @@
import 'dart:convert';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../domain/category/category.dart';
part 'category_dtos.freezed.dart';
part 'category_dtos.g.dart';
part 'dtos/category_dto.dart';

View File

@ -0,0 +1,675 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'category_dtos.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
);
ListCategoryDto _$ListCategoryDtoFromJson(Map<String, dynamic> json) {
return _ListCategoryDto.fromJson(json);
}
/// @nodoc
mixin _$ListCategoryDto {
@JsonKey(name: "categories")
List<CategoryDto>? get categories => throw _privateConstructorUsedError;
@JsonKey(name: "total_count")
int? get totalCount => throw _privateConstructorUsedError;
@JsonKey(name: "page")
int? get page => throw _privateConstructorUsedError;
@JsonKey(name: "limit")
int? get limit => throw _privateConstructorUsedError;
@JsonKey(name: "total_pages")
int? get totalPages => throw _privateConstructorUsedError;
/// Serializes this ListCategoryDto to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of ListCategoryDto
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$ListCategoryDtoCopyWith<ListCategoryDto> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ListCategoryDtoCopyWith<$Res> {
factory $ListCategoryDtoCopyWith(
ListCategoryDto value,
$Res Function(ListCategoryDto) then,
) = _$ListCategoryDtoCopyWithImpl<$Res, ListCategoryDto>;
@useResult
$Res call({
@JsonKey(name: "categories") List<CategoryDto>? categories,
@JsonKey(name: "total_count") int? totalCount,
@JsonKey(name: "page") int? page,
@JsonKey(name: "limit") int? limit,
@JsonKey(name: "total_pages") int? totalPages,
});
}
/// @nodoc
class _$ListCategoryDtoCopyWithImpl<$Res, $Val extends ListCategoryDto>
implements $ListCategoryDtoCopyWith<$Res> {
_$ListCategoryDtoCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of ListCategoryDto
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? categories = freezed,
Object? totalCount = freezed,
Object? page = freezed,
Object? limit = freezed,
Object? totalPages = freezed,
}) {
return _then(
_value.copyWith(
categories: freezed == categories
? _value.categories
: categories // ignore: cast_nullable_to_non_nullable
as List<CategoryDto>?,
totalCount: freezed == totalCount
? _value.totalCount
: totalCount // ignore: cast_nullable_to_non_nullable
as int?,
page: freezed == page
? _value.page
: page // ignore: cast_nullable_to_non_nullable
as int?,
limit: freezed == limit
? _value.limit
: limit // ignore: cast_nullable_to_non_nullable
as int?,
totalPages: freezed == totalPages
? _value.totalPages
: totalPages // ignore: cast_nullable_to_non_nullable
as int?,
)
as $Val,
);
}
}
/// @nodoc
abstract class _$$ListCategoryDtoImplCopyWith<$Res>
implements $ListCategoryDtoCopyWith<$Res> {
factory _$$ListCategoryDtoImplCopyWith(
_$ListCategoryDtoImpl value,
$Res Function(_$ListCategoryDtoImpl) then,
) = __$$ListCategoryDtoImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({
@JsonKey(name: "categories") List<CategoryDto>? categories,
@JsonKey(name: "total_count") int? totalCount,
@JsonKey(name: "page") int? page,
@JsonKey(name: "limit") int? limit,
@JsonKey(name: "total_pages") int? totalPages,
});
}
/// @nodoc
class __$$ListCategoryDtoImplCopyWithImpl<$Res>
extends _$ListCategoryDtoCopyWithImpl<$Res, _$ListCategoryDtoImpl>
implements _$$ListCategoryDtoImplCopyWith<$Res> {
__$$ListCategoryDtoImplCopyWithImpl(
_$ListCategoryDtoImpl _value,
$Res Function(_$ListCategoryDtoImpl) _then,
) : super(_value, _then);
/// Create a copy of ListCategoryDto
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? categories = freezed,
Object? totalCount = freezed,
Object? page = freezed,
Object? limit = freezed,
Object? totalPages = freezed,
}) {
return _then(
_$ListCategoryDtoImpl(
categories: freezed == categories
? _value._categories
: categories // ignore: cast_nullable_to_non_nullable
as List<CategoryDto>?,
totalCount: freezed == totalCount
? _value.totalCount
: totalCount // ignore: cast_nullable_to_non_nullable
as int?,
page: freezed == page
? _value.page
: page // ignore: cast_nullable_to_non_nullable
as int?,
limit: freezed == limit
? _value.limit
: limit // ignore: cast_nullable_to_non_nullable
as int?,
totalPages: freezed == totalPages
? _value.totalPages
: totalPages // ignore: cast_nullable_to_non_nullable
as int?,
),
);
}
}
/// @nodoc
@JsonSerializable()
class _$ListCategoryDtoImpl extends _ListCategoryDto {
const _$ListCategoryDtoImpl({
@JsonKey(name: "categories") final List<CategoryDto>? categories,
@JsonKey(name: "total_count") this.totalCount,
@JsonKey(name: "page") this.page,
@JsonKey(name: "limit") this.limit,
@JsonKey(name: "total_pages") this.totalPages,
}) : _categories = categories,
super._();
factory _$ListCategoryDtoImpl.fromJson(Map<String, dynamic> json) =>
_$$ListCategoryDtoImplFromJson(json);
final List<CategoryDto>? _categories;
@override
@JsonKey(name: "categories")
List<CategoryDto>? get categories {
final value = _categories;
if (value == null) return null;
if (_categories is EqualUnmodifiableListView) return _categories;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
@override
@JsonKey(name: "total_count")
final int? totalCount;
@override
@JsonKey(name: "page")
final int? page;
@override
@JsonKey(name: "limit")
final int? limit;
@override
@JsonKey(name: "total_pages")
final int? totalPages;
@override
String toString() {
return 'ListCategoryDto(categories: $categories, totalCount: $totalCount, page: $page, limit: $limit, totalPages: $totalPages)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ListCategoryDtoImpl &&
const DeepCollectionEquality().equals(
other._categories,
_categories,
) &&
(identical(other.totalCount, totalCount) ||
other.totalCount == totalCount) &&
(identical(other.page, page) || other.page == page) &&
(identical(other.limit, limit) || other.limit == limit) &&
(identical(other.totalPages, totalPages) ||
other.totalPages == totalPages));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(_categories),
totalCount,
page,
limit,
totalPages,
);
/// Create a copy of ListCategoryDto
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$ListCategoryDtoImplCopyWith<_$ListCategoryDtoImpl> get copyWith =>
__$$ListCategoryDtoImplCopyWithImpl<_$ListCategoryDtoImpl>(
this,
_$identity,
);
@override
Map<String, dynamic> toJson() {
return _$$ListCategoryDtoImplToJson(this);
}
}
abstract class _ListCategoryDto extends ListCategoryDto {
const factory _ListCategoryDto({
@JsonKey(name: "categories") final List<CategoryDto>? categories,
@JsonKey(name: "total_count") final int? totalCount,
@JsonKey(name: "page") final int? page,
@JsonKey(name: "limit") final int? limit,
@JsonKey(name: "total_pages") final int? totalPages,
}) = _$ListCategoryDtoImpl;
const _ListCategoryDto._() : super._();
factory _ListCategoryDto.fromJson(Map<String, dynamic> json) =
_$ListCategoryDtoImpl.fromJson;
@override
@JsonKey(name: "categories")
List<CategoryDto>? get categories;
@override
@JsonKey(name: "total_count")
int? get totalCount;
@override
@JsonKey(name: "page")
int? get page;
@override
@JsonKey(name: "limit")
int? get limit;
@override
@JsonKey(name: "total_pages")
int? get totalPages;
/// Create a copy of ListCategoryDto
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$ListCategoryDtoImplCopyWith<_$ListCategoryDtoImpl> get copyWith =>
throw _privateConstructorUsedError;
}
CategoryDto _$CategoryDtoFromJson(Map<String, dynamic> json) {
return _CategoryDto.fromJson(json);
}
/// @nodoc
mixin _$CategoryDto {
@JsonKey(name: "id")
String? get id => throw _privateConstructorUsedError;
@JsonKey(name: "organization_id")
String? get organizationId => throw _privateConstructorUsedError;
@JsonKey(name: "name")
String? get name => throw _privateConstructorUsedError;
@JsonKey(name: "description")
String? get description => throw _privateConstructorUsedError;
@JsonKey(name: "business_type")
String? get businessType => throw _privateConstructorUsedError;
@JsonKey(name: "order")
int? get order => throw _privateConstructorUsedError;
@JsonKey(name: "metadata")
Map<String, dynamic>? get metadata => throw _privateConstructorUsedError;
@JsonKey(name: "created_at")
String? get createdAt => throw _privateConstructorUsedError;
@JsonKey(name: "updated_at")
String? get updatedAt => throw _privateConstructorUsedError;
/// Serializes this CategoryDto to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of CategoryDto
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$CategoryDtoCopyWith<CategoryDto> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $CategoryDtoCopyWith<$Res> {
factory $CategoryDtoCopyWith(
CategoryDto value,
$Res Function(CategoryDto) then,
) = _$CategoryDtoCopyWithImpl<$Res, CategoryDto>;
@useResult
$Res call({
@JsonKey(name: "id") String? id,
@JsonKey(name: "organization_id") String? organizationId,
@JsonKey(name: "name") String? name,
@JsonKey(name: "description") String? description,
@JsonKey(name: "business_type") String? businessType,
@JsonKey(name: "order") int? order,
@JsonKey(name: "metadata") Map<String, dynamic>? metadata,
@JsonKey(name: "created_at") String? createdAt,
@JsonKey(name: "updated_at") String? updatedAt,
});
}
/// @nodoc
class _$CategoryDtoCopyWithImpl<$Res, $Val extends CategoryDto>
implements $CategoryDtoCopyWith<$Res> {
_$CategoryDtoCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of CategoryDto
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = freezed,
Object? organizationId = freezed,
Object? name = freezed,
Object? description = freezed,
Object? businessType = freezed,
Object? order = freezed,
Object? metadata = freezed,
Object? createdAt = freezed,
Object? updatedAt = freezed,
}) {
return _then(
_value.copyWith(
id: freezed == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String?,
organizationId: freezed == organizationId
? _value.organizationId
: organizationId // ignore: cast_nullable_to_non_nullable
as String?,
name: freezed == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String?,
description: freezed == description
? _value.description
: description // ignore: cast_nullable_to_non_nullable
as String?,
businessType: freezed == businessType
? _value.businessType
: businessType // ignore: cast_nullable_to_non_nullable
as String?,
order: freezed == order
? _value.order
: order // ignore: cast_nullable_to_non_nullable
as int?,
metadata: freezed == metadata
? _value.metadata
: metadata // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>?,
createdAt: freezed == createdAt
? _value.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as String?,
updatedAt: freezed == updatedAt
? _value.updatedAt
: updatedAt // ignore: cast_nullable_to_non_nullable
as String?,
)
as $Val,
);
}
}
/// @nodoc
abstract class _$$CategoryDtoImplCopyWith<$Res>
implements $CategoryDtoCopyWith<$Res> {
factory _$$CategoryDtoImplCopyWith(
_$CategoryDtoImpl value,
$Res Function(_$CategoryDtoImpl) then,
) = __$$CategoryDtoImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({
@JsonKey(name: "id") String? id,
@JsonKey(name: "organization_id") String? organizationId,
@JsonKey(name: "name") String? name,
@JsonKey(name: "description") String? description,
@JsonKey(name: "business_type") String? businessType,
@JsonKey(name: "order") int? order,
@JsonKey(name: "metadata") Map<String, dynamic>? metadata,
@JsonKey(name: "created_at") String? createdAt,
@JsonKey(name: "updated_at") String? updatedAt,
});
}
/// @nodoc
class __$$CategoryDtoImplCopyWithImpl<$Res>
extends _$CategoryDtoCopyWithImpl<$Res, _$CategoryDtoImpl>
implements _$$CategoryDtoImplCopyWith<$Res> {
__$$CategoryDtoImplCopyWithImpl(
_$CategoryDtoImpl _value,
$Res Function(_$CategoryDtoImpl) _then,
) : super(_value, _then);
/// Create a copy of CategoryDto
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? id = freezed,
Object? organizationId = freezed,
Object? name = freezed,
Object? description = freezed,
Object? businessType = freezed,
Object? order = freezed,
Object? metadata = freezed,
Object? createdAt = freezed,
Object? updatedAt = freezed,
}) {
return _then(
_$CategoryDtoImpl(
id: freezed == id
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String?,
organizationId: freezed == organizationId
? _value.organizationId
: organizationId // ignore: cast_nullable_to_non_nullable
as String?,
name: freezed == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String?,
description: freezed == description
? _value.description
: description // ignore: cast_nullable_to_non_nullable
as String?,
businessType: freezed == businessType
? _value.businessType
: businessType // ignore: cast_nullable_to_non_nullable
as String?,
order: freezed == order
? _value.order
: order // ignore: cast_nullable_to_non_nullable
as int?,
metadata: freezed == metadata
? _value._metadata
: metadata // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>?,
createdAt: freezed == createdAt
? _value.createdAt
: createdAt // ignore: cast_nullable_to_non_nullable
as String?,
updatedAt: freezed == updatedAt
? _value.updatedAt
: updatedAt // ignore: cast_nullable_to_non_nullable
as String?,
),
);
}
}
/// @nodoc
@JsonSerializable()
class _$CategoryDtoImpl extends _CategoryDto {
const _$CategoryDtoImpl({
@JsonKey(name: "id") this.id,
@JsonKey(name: "organization_id") this.organizationId,
@JsonKey(name: "name") this.name,
@JsonKey(name: "description") this.description,
@JsonKey(name: "business_type") this.businessType,
@JsonKey(name: "order") this.order,
@JsonKey(name: "metadata") final Map<String, dynamic>? metadata,
@JsonKey(name: "created_at") this.createdAt,
@JsonKey(name: "updated_at") this.updatedAt,
}) : _metadata = metadata,
super._();
factory _$CategoryDtoImpl.fromJson(Map<String, dynamic> json) =>
_$$CategoryDtoImplFromJson(json);
@override
@JsonKey(name: "id")
final String? id;
@override
@JsonKey(name: "organization_id")
final String? organizationId;
@override
@JsonKey(name: "name")
final String? name;
@override
@JsonKey(name: "description")
final String? description;
@override
@JsonKey(name: "business_type")
final String? businessType;
@override
@JsonKey(name: "order")
final int? order;
final Map<String, dynamic>? _metadata;
@override
@JsonKey(name: "metadata")
Map<String, dynamic>? get metadata {
final value = _metadata;
if (value == null) return null;
if (_metadata is EqualUnmodifiableMapView) return _metadata;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(value);
}
@override
@JsonKey(name: "created_at")
final String? createdAt;
@override
@JsonKey(name: "updated_at")
final String? updatedAt;
@override
String toString() {
return 'CategoryDto(id: $id, organizationId: $organizationId, name: $name, description: $description, businessType: $businessType, order: $order, metadata: $metadata, createdAt: $createdAt, updatedAt: $updatedAt)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$CategoryDtoImpl &&
(identical(other.id, id) || other.id == id) &&
(identical(other.organizationId, organizationId) ||
other.organizationId == organizationId) &&
(identical(other.name, name) || other.name == name) &&
(identical(other.description, description) ||
other.description == description) &&
(identical(other.businessType, businessType) ||
other.businessType == businessType) &&
(identical(other.order, order) || other.order == order) &&
const DeepCollectionEquality().equals(other._metadata, _metadata) &&
(identical(other.createdAt, createdAt) ||
other.createdAt == createdAt) &&
(identical(other.updatedAt, updatedAt) ||
other.updatedAt == updatedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
id,
organizationId,
name,
description,
businessType,
order,
const DeepCollectionEquality().hash(_metadata),
createdAt,
updatedAt,
);
/// Create a copy of CategoryDto
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$CategoryDtoImplCopyWith<_$CategoryDtoImpl> get copyWith =>
__$$CategoryDtoImplCopyWithImpl<_$CategoryDtoImpl>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$CategoryDtoImplToJson(this);
}
}
abstract class _CategoryDto extends CategoryDto {
const factory _CategoryDto({
@JsonKey(name: "id") final String? id,
@JsonKey(name: "organization_id") final String? organizationId,
@JsonKey(name: "name") final String? name,
@JsonKey(name: "description") final String? description,
@JsonKey(name: "business_type") final String? businessType,
@JsonKey(name: "order") final int? order,
@JsonKey(name: "metadata") final Map<String, dynamic>? metadata,
@JsonKey(name: "created_at") final String? createdAt,
@JsonKey(name: "updated_at") final String? updatedAt,
}) = _$CategoryDtoImpl;
const _CategoryDto._() : super._();
factory _CategoryDto.fromJson(Map<String, dynamic> json) =
_$CategoryDtoImpl.fromJson;
@override
@JsonKey(name: "id")
String? get id;
@override
@JsonKey(name: "organization_id")
String? get organizationId;
@override
@JsonKey(name: "name")
String? get name;
@override
@JsonKey(name: "description")
String? get description;
@override
@JsonKey(name: "business_type")
String? get businessType;
@override
@JsonKey(name: "order")
int? get order;
@override
@JsonKey(name: "metadata")
Map<String, dynamic>? get metadata;
@override
@JsonKey(name: "created_at")
String? get createdAt;
@override
@JsonKey(name: "updated_at")
String? get updatedAt;
/// Create a copy of CategoryDto
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$CategoryDtoImplCopyWith<_$CategoryDtoImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -0,0 +1,55 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'category_dtos.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$ListCategoryDtoImpl _$$ListCategoryDtoImplFromJson(
Map<String, dynamic> json,
) => _$ListCategoryDtoImpl(
categories: (json['categories'] as List<dynamic>?)
?.map((e) => CategoryDto.fromJson(e as Map<String, dynamic>))
.toList(),
totalCount: (json['total_count'] as num?)?.toInt(),
page: (json['page'] as num?)?.toInt(),
limit: (json['limit'] as num?)?.toInt(),
totalPages: (json['total_pages'] as num?)?.toInt(),
);
Map<String, dynamic> _$$ListCategoryDtoImplToJson(
_$ListCategoryDtoImpl instance,
) => <String, dynamic>{
'categories': instance.categories,
'total_count': instance.totalCount,
'page': instance.page,
'limit': instance.limit,
'total_pages': instance.totalPages,
};
_$CategoryDtoImpl _$$CategoryDtoImplFromJson(Map<String, dynamic> json) =>
_$CategoryDtoImpl(
id: json['id'] as String?,
organizationId: json['organization_id'] as String?,
name: json['name'] as String?,
description: json['description'] as String?,
businessType: json['business_type'] as String?,
order: (json['order'] as num?)?.toInt(),
metadata: json['metadata'] as Map<String, dynamic>?,
createdAt: json['created_at'] as String?,
updatedAt: json['updated_at'] as String?,
);
Map<String, dynamic> _$$CategoryDtoImplToJson(_$CategoryDtoImpl instance) =>
<String, dynamic>{
'id': instance.id,
'organization_id': instance.organizationId,
'name': instance.name,
'description': instance.description,
'business_type': instance.businessType,
'order': instance.order,
'metadata': instance.metadata,
'created_at': instance.createdAt,
'updated_at': instance.updatedAt,
};

View File

@ -0,0 +1,349 @@
import 'dart:developer';
import 'package:data_channel/data_channel.dart';
import 'package:injectable/injectable.dart';
import 'package:sqflite/sqflite.dart';
import '../../../common/constant/app_constant.dart';
import '../../../common/database/database_helper.dart';
import '../../../domain/category/category.dart';
import '../category_dtos.dart';
@injectable
class CategoryLocalDataProvider {
final DatabaseHelper _databaseHelper;
final _logName = 'CategoryLocalDataProvider';
CategoryLocalDataProvider(this._databaseHelper);
final Map<String, List<CategoryDto>> _queryCache = {};
final Duration _cacheExpiry = Duration(minutes: AppConstant.cacheExpire);
final Map<String, DateTime> _cacheTimestamps = {};
Future<DC<CategoryFailure, void>> saveCategoriesBatch(
List<CategoryDto> categories, {
bool clearFirst = false,
}) async {
final db = await _databaseHelper.database;
try {
await db.transaction((txn) async {
if (clearFirst) {
log('🗑️ Clearing existing categories...', name: _logName);
await txn.delete('categories');
}
log(
'💾 Batch saving ${categories.length} categories...',
name: _logName,
);
// Batch insert categories
final batch = txn.batch();
for (final category in categories) {
batch.insert(
'categories',
category.toMap(),
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
await batch.commit(noResult: true);
});
// Clear cache after update
clearCache();
log(
'✅ Successfully batch saved ${categories.length} categories',
name: _logName,
);
return DC.data(null);
} catch (e, s) {
log(
'❌ Error batch saving categories',
name: _logName,
error: e,
stackTrace: s,
);
return DC.error(CategoryFailure.dynamicErrorMessage(e.toString()));
}
}
Future<DC<CategoryFailure, List<CategoryDto>>> getCachedCategories({
int page = 1,
int limit = 10,
bool isActive = true,
String? search,
}) async {
final cacheKey = _generateCacheKey(page, limit, isActive, search);
final now = DateTime.now();
try {
// Check cache first
if (_queryCache.containsKey(cacheKey) &&
_cacheTimestamps.containsKey(cacheKey)) {
final cacheTime = _cacheTimestamps[cacheKey]!;
if (now.difference(cacheTime) < _cacheExpiry) {
log(
'🚀 Cache HIT: $cacheKey (${_queryCache[cacheKey]!.length} categories)',
name: _logName,
);
return DC.data(_queryCache[cacheKey]!);
}
}
log('📀 Cache MISS: $cacheKey, querying database...', name: _logName);
// Cache miss, query database
final result = await getCategories(
page: page,
limit: limit,
isActive: isActive,
search: search,
);
// Check if result has data or error
if (result.hasData) {
final categories = result.data!;
// Store in cache
_queryCache[cacheKey] = categories;
_cacheTimestamps[cacheKey] = now;
log(
'💾 Cached ${categories.length} categories for key: $cacheKey',
name: _logName,
);
return DC.data(categories);
} else {
// Return error from database query
return DC.error(result.error!);
}
} catch (e, s) {
log(
'❌ Error getting cached categories',
name: _logName,
error: e,
stackTrace: s,
);
return DC.error(CategoryFailure.localStorageError(e.toString()));
}
}
Future<DC<CategoryFailure, List<CategoryDto>>> getCategories({
int page = 1,
int limit = 10,
bool isActive = true,
String? search,
}) async {
final db = await _databaseHelper.database;
try {
String query = 'SELECT * FROM categories WHERE 1=1';
List<dynamic> whereArgs = [];
// Note: Assuming is_active will be added to database schema
if (isActive) {
query += ' AND is_active = ?';
whereArgs.add(1);
}
if (search != null && search.isNotEmpty) {
query += ' AND (name LIKE ? OR description LIKE ?)';
whereArgs.add('%$search%');
whereArgs.add('%$search%');
}
// query += ' ORDER BY name ASC';
if (limit > 0) {
query += ' LIMIT ?';
whereArgs.add(limit);
if (page > 1) {
query += ' OFFSET ?';
whereArgs.add((page - 1) * limit);
}
}
final List<Map<String, dynamic>> maps = await db.rawQuery(
query,
whereArgs,
);
final categories = maps.map((map) => CategoryDto.fromMap(map)).toList();
log(
'📊 Retrieved ${categories.length} categories from database',
name: _logName,
);
return DC.data(categories);
} catch (e, s) {
log(
'❌ Error getting categories',
name: _logName,
error: e,
stackTrace: s,
);
return DC.error(CategoryFailure.localStorageError(e.toString()));
}
}
Future<DC<CategoryFailure, CategoryDto>> getCategoryById(String id) async {
final db = await _databaseHelper.database;
try {
final List<Map<String, dynamic>> maps = await db.query(
'categories',
where: 'id = ?',
whereArgs: [id],
);
if (maps.isEmpty) {
log('❌ Category not found: $id', name: _logName);
return DC.error(CategoryFailure.empty());
}
final category = CategoryDto.fromMap(maps.first);
log('✅ Category found: ${category.name}', name: _logName);
return DC.data(category);
} catch (e, s) {
log(
'❌ Error getting category by ID',
name: _logName,
error: e,
stackTrace: s,
);
return DC.error(CategoryFailure.localStorageError(e.toString()));
}
}
Future<int> getTotalCount({bool isActive = true, String? search}) async {
final db = await _databaseHelper.database;
try {
String query = 'SELECT COUNT(*) FROM categories WHERE 1=1';
List<dynamic> whereArgs = [];
if (isActive) {
query += ' AND is_active = ?';
whereArgs.add(1);
}
if (search != null && search.isNotEmpty) {
query += ' AND (name LIKE ? OR description LIKE ?)';
whereArgs.add('%$search%');
whereArgs.add('%$search%');
}
final result = await db.rawQuery(query, whereArgs);
final count = Sqflite.firstIntValue(result) ?? 0;
log(
'📊 Category total count: $count (isActive: $isActive, search: $search)',
name: _logName,
);
return count;
} catch (e) {
log('❌ Error getting category total count: $e', name: _logName);
return 0;
}
}
Future<bool> hasCategories() async {
final count = await getTotalCount();
final hasData = count > 0;
log('🔍 Has categories: $hasData ($count categories)', name: _logName);
return hasData;
}
Future<DC<CategoryFailure, Map<String, dynamic>>> getDatabaseStats() async {
final db = await _databaseHelper.database;
try {
final categoryCount =
Sqflite.firstIntValue(
await db.rawQuery('SELECT COUNT(*) FROM categories'),
) ??
0;
final activeCount =
Sqflite.firstIntValue(
await db.rawQuery(
'SELECT COUNT(*) FROM categories WHERE is_active = 1',
),
) ??
0;
final stats = {
'total_categories': categoryCount,
'active_categories': activeCount,
'cache_entries': _queryCache.length,
'last_updated': DateTime.now().toIso8601String(),
};
log('📊 Category Database Stats: $stats', name: _logName);
return DC.data(stats);
} catch (e, s) {
log(
'❌ Error getting category database stats',
name: _logName,
error: e,
stackTrace: s,
);
return DC.error(
CategoryFailure.localStorageError(
'Gagal memuat statistik database: $e',
),
);
}
}
Future<void> clearAllCategories() async {
final db = await _databaseHelper.database;
try {
await db.delete('categories');
clearCache();
log('🗑️ All categories cleared from local DB');
} catch (e) {
log('❌ Error clearing categories: $e');
rethrow;
}
}
void clearCache() {
final count = _queryCache.length;
_queryCache.clear();
_cacheTimestamps.clear();
log('🧹 Category cache cleared: $count entries removed', name: _logName);
}
String _generateCacheKey(int page, int limit, bool isActive, String? search) {
return 'categories_${page}_${limit}_${isActive}_${search ?? 'null'}';
}
void clearExpiredCache() {
final now = DateTime.now();
final expiredKeys = <String>[];
_cacheTimestamps.forEach((key, timestamp) {
if (now.difference(timestamp) > _cacheExpiry) {
expiredKeys.add(key);
}
});
for (final key in expiredKeys) {
_queryCache.remove(key);
_cacheTimestamps.remove(key);
}
if (expiredKeys.isNotEmpty) {
log(
'⏰ Expired category cache cleared: ${expiredKeys.length} entries',
name: _logName,
);
}
}
}

View File

@ -0,0 +1,45 @@
import 'dart:developer';
import 'package:data_channel/data_channel.dart';
import 'package:injectable/injectable.dart';
import '../../../common/api/api_client.dart';
import '../../../common/api/api_failure.dart';
import '../../../common/function/app_function.dart';
import '../../../common/url/api_path.dart';
import '../../../domain/category/category.dart';
import '../category_dtos.dart';
@injectable
class CategoryRemoteDataProvider {
final ApiClient _apiClient;
final _logName = 'CategoryRemoteDataProvider';
CategoryRemoteDataProvider(this._apiClient);
Future<DC<CategoryFailure, ListCategoryDto>> fetchCategories({
int page = 1,
int limit = 10,
}) async {
try {
final response = await _apiClient.get(
ApiPath.categories,
params: {'page': page, 'limit': limit},
headers: getAuthorizationHeader(),
);
if (response.data['data'] == null) {
return DC.error(CategoryFailure.empty());
}
final categories = ListCategoryDto.fromJson(
response.data['data'] as Map<String, dynamic>,
);
return DC.data(categories);
} on ApiFailure catch (e, s) {
log('fetchCategoryError', name: _logName, error: e, stackTrace: s);
return DC.error(CategoryFailure.serverError(e));
}
}
}

View File

@ -0,0 +1,98 @@
part of '../category_dtos.dart';
@freezed
class ListCategoryDto with _$ListCategoryDto {
const ListCategoryDto._();
const factory ListCategoryDto({
@JsonKey(name: "categories") List<CategoryDto>? categories,
@JsonKey(name: "total_count") int? totalCount,
@JsonKey(name: "page") int? page,
@JsonKey(name: "limit") int? limit,
@JsonKey(name: "total_pages") int? totalPages,
}) = _ListCategoryDto;
factory ListCategoryDto.fromJson(Map<String, dynamic> json) =>
_$ListCategoryDtoFromJson(json);
ListCategory toDomain() => ListCategory(
categories: categories?.map((dto) => dto.toDomain()).toList() ?? [],
totalCount: totalCount ?? 0,
page: page ?? 0,
limit: limit ?? 0,
totalPages: totalPages ?? 0,
);
}
@freezed
class CategoryDto with _$CategoryDto {
const CategoryDto._();
const factory CategoryDto({
@JsonKey(name: "id") String? id,
@JsonKey(name: "organization_id") String? organizationId,
@JsonKey(name: "name") String? name,
@JsonKey(name: "description") String? description,
@JsonKey(name: "business_type") String? businessType,
@JsonKey(name: "order") int? order,
@JsonKey(name: "metadata") Map<String, dynamic>? metadata,
@JsonKey(name: "created_at") String? createdAt,
@JsonKey(name: "updated_at") String? updatedAt,
}) = _CategoryDto;
factory CategoryDto.fromJson(Map<String, dynamic> json) =>
_$CategoryDtoFromJson(json);
/// Mapping ke domain
Category toDomain() => Category(
id: id ?? '',
organizationId: organizationId ?? '',
name: name ?? '',
description: description ?? '',
businessType: businessType ?? '',
order: order ?? 0,
metadata: metadata ?? {},
createdAt: createdAt ?? '',
updatedAt: updatedAt ?? '',
);
/// Mapping ke Map untuk SQLite
Map<String, dynamic> toMap() => {
'id': id,
'organization_id': organizationId,
'name': name,
'description': description,
'business_type': businessType,
'order': order,
'metadata': metadata != null ? jsonEncode(metadata) : null,
'created_at': createdAt,
'updated_at': updatedAt,
};
/// Mapping dari Map SQLite
factory CategoryDto.fromMap(Map<String, dynamic> map) => CategoryDto(
id: map['id'] as String?,
organizationId: map['organization_id'] as String?,
name: map['name'] as String?,
description: map['description'] as String?,
businessType: map['business_type'] as String?,
order: map['order'] as int?,
metadata: map['metadata'] != null
? jsonDecode(map['metadata'] as String) as Map<String, dynamic>
: null,
createdAt: map['created_at'] as String?,
updatedAt: map['updated_at'] as String?,
);
factory CategoryDto.fromDomain(Category category) => CategoryDto(
id: category.id,
organizationId: category.organizationId,
name: category.name,
description: category.description,
businessType: category.businessType,
order: category.order,
metadata: category.metadata,
createdAt: category.createdAt,
updatedAt: category.updatedAt,
);
}

View File

@ -0,0 +1,352 @@
import 'dart:developer';
import 'package:dartz/dartz.dart';
import 'package:injectable/injectable.dart';
import '../../../domain/category/category.dart';
import '../category_dtos.dart';
import '../datasources/local_data_provider.dart';
import '../datasources/remote_data_provider.dart';
@Injectable(as: ICategoryRepository)
class CategoryRepository implements ICategoryRepository {
final CategoryRemoteDataProvider _remoteDataProvider;
final CategoryLocalDataProvider _localDataProvider;
final _logName = 'CategoryRepository';
CategoryRepository(this._remoteDataProvider, this._localDataProvider);
@override
Future<Either<CategoryFailure, ListCategory>> getCategories({
int page = 1,
int limit = 10,
bool isActive = true,
String? search,
bool forceRemote = false,
}) async {
try {
log(
'📱 Getting categories - page: $page, isActive: $isActive, search: $search, forceRemote: $forceRemote',
name: _logName,
);
// Clean expired cache
_localDataProvider.clearExpiredCache();
// Check if we should try remote first
if (forceRemote || !await _localDataProvider.hasCategories()) {
log('🌐 Attempting remote fetch first...', name: _logName);
final remoteResult = await _getRemoteCategories(
page: page,
limit: limit,
isActive: isActive,
);
return await remoteResult.fold(
(failure) async {
log('❌ Remote fetch failed: $failure', name: _logName);
log('📱 Falling back to local data...', name: _logName);
return await _getLocalCategories(
page: page,
limit: limit,
isActive: isActive,
search: search,
);
},
(data) async {
log(
'✅ Remote fetch successful, syncing to local...',
name: _logName,
);
// Sync remote data to local
if (data.categories.isNotEmpty) {
await _syncToLocal(data.categories, clearFirst: page == 1);
}
return Right(data);
},
);
} else {
log('📱 Using local data (cache available)...', name: _logName);
return await _getLocalCategories(
page: page,
limit: limit,
isActive: isActive,
search: search,
);
}
} catch (e, s) {
log('❌ Error in getCategories', name: _logName, error: e, stackTrace: s);
return Left(
CategoryFailure.dynamicErrorMessage('Gagal memuat kategori: $e'),
);
}
}
@override
Future<Either<CategoryFailure, Category>> getCategoryById(String id) async {
try {
log('🔍 Getting category by ID: $id', name: _logName);
final result = await _localDataProvider.getCategoryById(id);
if (result.hasData) {
final category = result.data!.toDomain();
log('✅ Category found: ${category.name}', name: _logName);
return Right(category);
} else {
log('❌ Category not found or error: ${result.error}', name: _logName);
return Left(result.error!);
}
} catch (e, s) {
log(
'❌ Error getting category by ID',
name: _logName,
error: e,
stackTrace: s,
);
return Left(
CategoryFailure.localStorageError('Gagal memuat kategori: $e'),
);
}
}
@override
Future<Either<CategoryFailure, Map<String, dynamic>>>
getDatabaseStats() async {
try {
log('📊 Getting database stats...', name: _logName);
final result = await _localDataProvider.getDatabaseStats();
if (result.hasData) {
final stats = result.data!;
log('📊 Category database stats: $stats', name: _logName);
return Right(stats);
} else {
log('❌ Error getting stats: ${result.error}', name: _logName);
return Left(result.error!);
}
} catch (e, s) {
log(
'❌ Error getting database stats',
name: _logName,
error: e,
stackTrace: s,
);
return Left(
CategoryFailure.localStorageError(
'Gagal memuat statistik database: $e',
),
);
}
}
@override
Future<bool> hasLocalCategories() async {
final hasCategories = await _localDataProvider.hasCategories();
log('📊 Has local categories: $hasCategories', name: _logName);
return hasCategories;
}
@override
Future<Either<CategoryFailure, ListCategory>> refreshCategories({
bool isActive = true,
String? search,
}) async {
try {
log('🔄 Refreshing categories...', name: _logName);
// Clear cache before refresh
_localDataProvider.clearCache();
return await getCategories(
page: 1,
limit: 10,
isActive: isActive,
search: search,
forceRemote: true, // Force remote refresh
);
} catch (e, s) {
log(
'❌ Error refreshing categories',
name: _logName,
error: e,
stackTrace: s,
);
return Left(
CategoryFailure.localStorageError('Gagal memperbarui kategori: $e'),
);
}
}
@override
Future<Either<CategoryFailure, String>> syncAllCategories() async {
try {
log('🔄 Starting manual sync of all categories...', name: _logName);
int page = 1;
const limit = 50; // Higher limit for bulk sync
bool hasMore = true;
int totalSynced = 0;
// Clear local data first for fresh sync
await _localDataProvider.clearAllCategories();
while (hasMore) {
log('📄 Syncing page $page...');
final result = await _remoteDataProvider.fetchCategories(
page: page,
limit: limit,
// isActive: true,
);
// If fetchCategories returns DC directly, handle it directly
final data = result.data!.toDomain();
if (data.categories.isNotEmpty) {
await _localDataProvider.saveCategoriesBatch(
data.categories
.map((category) => CategoryDto.fromDomain(category))
.toList(),
clearFirst: false, // Don't clear on subsequent pages
);
totalSynced += data.categories.length;
// Check if we have more pages
hasMore = page < data.totalPages;
page++;
log(
'📦 Page ${page - 1} synced: ${data.categories.length} categories',
);
} else {
hasMore = false;
}
}
final message = 'Berhasil sinkronisasi $totalSynced kategori';
log('$message');
return Right(message);
} catch (e) {
final error = 'Gagal sinkronisasi kategori: $e';
log('$error');
return Left(CategoryFailure.localStorageError(error));
}
}
Future<Either<CategoryFailure, ListCategory>> _getRemoteCategories({
int page = 1,
int limit = 10,
bool isActive = true,
}) async {
try {
log('🌐 Fetching categories from remote...', name: _logName);
final result = await _remoteDataProvider.fetchCategories(
page: page,
limit: limit,
);
// Convert DC to Either and DTO to Domain
if (result.hasData) {
final categories = result.data!.toDomain();
return Right(categories);
} else {
return Left(result.error!);
}
} catch (e, s) {
log('❌ Remote fetch error', name: _logName, error: e, stackTrace: s);
return Left(
CategoryFailure.dynamicErrorMessage(
'Gagal mengambil data dari server: $e',
),
);
}
}
Future<Either<CategoryFailure, ListCategory>> _getLocalCategories({
int page = 1,
int limit = 10,
bool isActive = true,
String? search,
}) async {
try {
log('💾 Fetching categories from local...', name: _logName);
final result = await _localDataProvider.getCachedCategories(
page: page,
limit: limit,
isActive: isActive,
search: search,
);
final totalCount = await _localDataProvider.getTotalCount(
isActive: isActive,
search: search,
);
// Convert DC to Either and DTO to Domain
if (result.hasData) {
final categories = result.data!.map((dto) => dto.toDomain()).toList();
final categoryData = ListCategory(
categories: categories,
totalCount: totalCount,
page: page,
limit: limit,
totalPages: totalCount > 0 ? (totalCount / limit).ceil() : 0,
);
log('✅ Returned ${categories.length} local categories', name: _logName);
return Right(categoryData);
} else {
log(
'❌ Error getting local categories: ${result.error}',
name: _logName,
);
return Left(result.error!);
}
} catch (e, s) {
log(
'❌ Error getting local categories',
name: _logName,
error: e,
stackTrace: s,
);
return Left(
CategoryFailure.localStorageError(
'Gagal memuat kategori dari database lokal: $e',
),
);
}
}
Future<void> _syncToLocal(
List<Category> categories, {
bool clearFirst = false,
}) async {
try {
log(
'💾 Syncing ${categories.length} categories to local database...',
name: _logName,
);
await _localDataProvider.saveCategoriesBatch(
categories.map((category) => CategoryDto.fromDomain(category)).toList(),
clearFirst: clearFirst,
);
log('✅ Categories synced to local successfully', name: _logName);
} catch (e) {
log('❌ Error syncing categories to local: $e', name: _logName);
rethrow;
}
}
@override
void clearCache() {
log('🧹 Clearing category cache', name: _logName);
_localDataProvider.clearCache();
}
}

View File

@ -15,14 +15,18 @@ import 'package:apskel_pos_flutter_v2/application/auth/login_form/login_form_blo
import 'package:apskel_pos_flutter_v2/application/outlet/outlet_loader/outlet_loader_bloc.dart' import 'package:apskel_pos_flutter_v2/application/outlet/outlet_loader/outlet_loader_bloc.dart'
as _i76; as _i76;
import 'package:apskel_pos_flutter_v2/common/api/api_client.dart' as _i457; import 'package:apskel_pos_flutter_v2/common/api/api_client.dart' as _i457;
import 'package:apskel_pos_flutter_v2/common/database/database_helper.dart'
as _i487;
import 'package:apskel_pos_flutter_v2/common/di/di_auto_route.dart' as _i729; import 'package:apskel_pos_flutter_v2/common/di/di_auto_route.dart' as _i729;
import 'package:apskel_pos_flutter_v2/common/di/di_connectivity.dart' as _i807; import 'package:apskel_pos_flutter_v2/common/di/di_connectivity.dart' as _i807;
import 'package:apskel_pos_flutter_v2/common/di/di_database.dart' as _i209;
import 'package:apskel_pos_flutter_v2/common/di/di_dio.dart' as _i86; import 'package:apskel_pos_flutter_v2/common/di/di_dio.dart' as _i86;
import 'package:apskel_pos_flutter_v2/common/di/di_shared_preferences.dart' import 'package:apskel_pos_flutter_v2/common/di/di_shared_preferences.dart'
as _i135; as _i135;
import 'package:apskel_pos_flutter_v2/common/network/network_client.dart' import 'package:apskel_pos_flutter_v2/common/network/network_client.dart'
as _i171; as _i171;
import 'package:apskel_pos_flutter_v2/domain/auth/auth.dart' as _i776; import 'package:apskel_pos_flutter_v2/domain/auth/auth.dart' as _i776;
import 'package:apskel_pos_flutter_v2/domain/category/category.dart' as _i502;
import 'package:apskel_pos_flutter_v2/domain/outlet/outlet.dart' as _i552; import 'package:apskel_pos_flutter_v2/domain/outlet/outlet.dart' as _i552;
import 'package:apskel_pos_flutter_v2/env.dart' as _i923; import 'package:apskel_pos_flutter_v2/env.dart' as _i923;
import 'package:apskel_pos_flutter_v2/infrastructure/auth/datasources/local_data_provider.dart' import 'package:apskel_pos_flutter_v2/infrastructure/auth/datasources/local_data_provider.dart'
@ -31,6 +35,12 @@ import 'package:apskel_pos_flutter_v2/infrastructure/auth/datasources/remote_dat
as _i370; as _i370;
import 'package:apskel_pos_flutter_v2/infrastructure/auth/repositories/auth_repository.dart' import 'package:apskel_pos_flutter_v2/infrastructure/auth/repositories/auth_repository.dart'
as _i941; as _i941;
import 'package:apskel_pos_flutter_v2/infrastructure/category/datasources/local_data_provider.dart'
as _i708;
import 'package:apskel_pos_flutter_v2/infrastructure/category/datasources/remote_data_provider.dart'
as _i856;
import 'package:apskel_pos_flutter_v2/infrastructure/category/repositories/category_repository.dart'
as _i604;
import 'package:apskel_pos_flutter_v2/infrastructure/outlet/datasources/local_data_provider.dart' import 'package:apskel_pos_flutter_v2/infrastructure/outlet/datasources/local_data_provider.dart'
as _i693; as _i693;
import 'package:apskel_pos_flutter_v2/infrastructure/outlet/datasources/remote_data_provider.dart' import 'package:apskel_pos_flutter_v2/infrastructure/outlet/datasources/remote_data_provider.dart'
@ -56,6 +66,7 @@ extension GetItInjectableX on _i174.GetIt {
}) async { }) async {
final gh = _i526.GetItHelper(this, environment, environmentFilter); final gh = _i526.GetItHelper(this, environment, environmentFilter);
final sharedPreferencesDi = _$SharedPreferencesDi(); final sharedPreferencesDi = _$SharedPreferencesDi();
final databaseDi = _$DatabaseDi();
final dioDi = _$DioDi(); final dioDi = _$DioDi();
final autoRouteDi = _$AutoRouteDi(); final autoRouteDi = _$AutoRouteDi();
final connectivityDi = _$ConnectivityDi(); final connectivityDi = _$ConnectivityDi();
@ -63,6 +74,7 @@ extension GetItInjectableX on _i174.GetIt {
() => sharedPreferencesDi.prefs, () => sharedPreferencesDi.prefs,
preResolve: true, preResolve: true,
); );
gh.singleton<_i487.DatabaseHelper>(() => databaseDi.databaseHelper);
gh.lazySingleton<_i361.Dio>(() => dioDi.dio); gh.lazySingleton<_i361.Dio>(() => dioDi.dio);
gh.lazySingleton<_i800.AppRouter>(() => autoRouteDi.appRouter); gh.lazySingleton<_i800.AppRouter>(() => autoRouteDi.appRouter);
gh.lazySingleton<_i895.Connectivity>(() => connectivityDi.connectivity); gh.lazySingleton<_i895.Connectivity>(() => connectivityDi.connectivity);
@ -70,6 +82,9 @@ extension GetItInjectableX on _i174.GetIt {
() => _i171.NetworkClient(gh<_i895.Connectivity>()), () => _i171.NetworkClient(gh<_i895.Connectivity>()),
); );
gh.factory<_i923.Env>(() => _i923.DevEnv(), registerFor: {_dev}); gh.factory<_i923.Env>(() => _i923.DevEnv(), registerFor: {_dev});
gh.factory<_i708.CategoryLocalDataProvider>(
() => _i708.CategoryLocalDataProvider(gh<_i487.DatabaseHelper>()),
);
gh.factory<_i204.AuthLocalDataProvider>( gh.factory<_i204.AuthLocalDataProvider>(
() => _i204.AuthLocalDataProvider(gh<_i460.SharedPreferences>()), () => _i204.AuthLocalDataProvider(gh<_i460.SharedPreferences>()),
); );
@ -80,6 +95,9 @@ extension GetItInjectableX on _i174.GetIt {
() => _i457.ApiClient(gh<_i361.Dio>(), gh<_i923.Env>()), () => _i457.ApiClient(gh<_i361.Dio>(), gh<_i923.Env>()),
); );
gh.factory<_i923.Env>(() => _i923.ProdEnv(), registerFor: {_prod}); gh.factory<_i923.Env>(() => _i923.ProdEnv(), registerFor: {_prod});
gh.factory<_i856.CategoryRemoteDataProvider>(
() => _i856.CategoryRemoteDataProvider(gh<_i457.ApiClient>()),
);
gh.factory<_i370.AuthRemoteDataProvider>( gh.factory<_i370.AuthRemoteDataProvider>(
() => _i370.AuthRemoteDataProvider(gh<_i457.ApiClient>()), () => _i370.AuthRemoteDataProvider(gh<_i457.ApiClient>()),
); );
@ -92,6 +110,12 @@ extension GetItInjectableX on _i174.GetIt {
gh<_i204.AuthLocalDataProvider>(), gh<_i204.AuthLocalDataProvider>(),
), ),
); );
gh.factory<_i502.ICategoryRepository>(
() => _i604.CategoryRepository(
gh<_i856.CategoryRemoteDataProvider>(),
gh<_i708.CategoryLocalDataProvider>(),
),
);
gh.factory<_i552.IOutletRepository>( gh.factory<_i552.IOutletRepository>(
() => _i845.OutletRepository( () => _i845.OutletRepository(
gh<_i132.OutletRemoteDataProvider>(), gh<_i132.OutletRemoteDataProvider>(),
@ -116,6 +140,8 @@ extension GetItInjectableX on _i174.GetIt {
class _$SharedPreferencesDi extends _i135.SharedPreferencesDi {} class _$SharedPreferencesDi extends _i135.SharedPreferencesDi {}
class _$DatabaseDi extends _i209.DatabaseDi {}
class _$DioDi extends _i86.DioDi {} class _$DioDi extends _i86.DioDi {}
class _$AutoRouteDi extends _i729.AutoRouteDi {} class _$AutoRouteDi extends _i729.AutoRouteDi {}

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../application/auth/auth_bloc.dart'; import '../application/auth/auth_bloc.dart';
import '../application/category/category_loader/category_loader_bloc.dart';
import '../application/outlet/outlet_loader/outlet_loader_bloc.dart'; import '../application/outlet/outlet_loader/outlet_loader_bloc.dart';
import '../common/theme/theme.dart'; import '../common/theme/theme.dart';
import '../common/constant/app_constant.dart'; import '../common/constant/app_constant.dart';
@ -25,6 +26,7 @@ class _AppWidgetState extends State<AppWidget> {
providers: [ providers: [
BlocProvider(create: (context) => getIt<AuthBloc>()), BlocProvider(create: (context) => getIt<AuthBloc>()),
BlocProvider(create: (context) => getIt<OutletLoaderBloc>()), BlocProvider(create: (context) => getIt<OutletLoaderBloc>()),
BlocProvider(create: (context) => getIt<CategoryLoaderBloc>()),
], ],
child: MaterialApp.router( child: MaterialApp.router(
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,

View File

@ -35,7 +35,7 @@ class _SplashPageState extends State<SplashPage> {
listenWhen: (previous, current) => previous.status != current.status, listenWhen: (previous, current) => previous.status != current.status,
listener: (context, state) { listener: (context, state) {
if (state.isAuthenticated) { if (state.isAuthenticated) {
context.router.replace(const MainRoute()); context.router.replace(const SyncRoute());
} else { } else {
context.router.replace(const LoginRoute()); context.router.replace(const LoginRoute());
} }

View File

@ -0,0 +1,12 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/widgets.dart';
@RoutePage()
class SyncPage extends StatelessWidget {
const SyncPage({super.key});
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}

View File

@ -22,5 +22,8 @@ class AppRouter extends RootStackRouter {
AutoRoute(page: SettingRoute.page), AutoRoute(page: SettingRoute.page),
], ],
), ),
// Sync
AutoRoute(page: SyncRoute.page),
]; ];
} }

View File

@ -22,20 +22,22 @@ import 'package:apskel_pos_flutter_v2/presentation/pages/main/pages/report/repor
import 'package:apskel_pos_flutter_v2/presentation/pages/main/pages/setting/setting_page.dart' import 'package:apskel_pos_flutter_v2/presentation/pages/main/pages/setting/setting_page.dart'
as _i6; as _i6;
import 'package:apskel_pos_flutter_v2/presentation/pages/main/pages/table/table_page.dart' import 'package:apskel_pos_flutter_v2/presentation/pages/main/pages/table/table_page.dart'
as _i8; as _i9;
import 'package:apskel_pos_flutter_v2/presentation/pages/splash/splash_page.dart' import 'package:apskel_pos_flutter_v2/presentation/pages/splash/splash_page.dart'
as _i7; as _i7;
import 'package:auto_route/auto_route.dart' as _i9; import 'package:apskel_pos_flutter_v2/presentation/pages/sync/sync_page.dart'
as _i8;
import 'package:auto_route/auto_route.dart' as _i10;
/// generated route for /// generated route for
/// [_i1.CustomerPage] /// [_i1.CustomerPage]
class CustomerRoute extends _i9.PageRouteInfo<void> { class CustomerRoute extends _i10.PageRouteInfo<void> {
const CustomerRoute({List<_i9.PageRouteInfo>? children}) const CustomerRoute({List<_i10.PageRouteInfo>? children})
: super(CustomerRoute.name, initialChildren: children); : super(CustomerRoute.name, initialChildren: children);
static const String name = 'CustomerRoute'; static const String name = 'CustomerRoute';
static _i9.PageInfo page = _i9.PageInfo( static _i10.PageInfo page = _i10.PageInfo(
name, name,
builder: (data) { builder: (data) {
return const _i1.CustomerPage(); return const _i1.CustomerPage();
@ -45,13 +47,13 @@ class CustomerRoute extends _i9.PageRouteInfo<void> {
/// generated route for /// generated route for
/// [_i2.HomePage] /// [_i2.HomePage]
class HomeRoute extends _i9.PageRouteInfo<void> { class HomeRoute extends _i10.PageRouteInfo<void> {
const HomeRoute({List<_i9.PageRouteInfo>? children}) const HomeRoute({List<_i10.PageRouteInfo>? children})
: super(HomeRoute.name, initialChildren: children); : super(HomeRoute.name, initialChildren: children);
static const String name = 'HomeRoute'; static const String name = 'HomeRoute';
static _i9.PageInfo page = _i9.PageInfo( static _i10.PageInfo page = _i10.PageInfo(
name, name,
builder: (data) { builder: (data) {
return const _i2.HomePage(); return const _i2.HomePage();
@ -61,29 +63,29 @@ class HomeRoute extends _i9.PageRouteInfo<void> {
/// generated route for /// generated route for
/// [_i3.LoginPage] /// [_i3.LoginPage]
class LoginRoute extends _i9.PageRouteInfo<void> { class LoginRoute extends _i10.PageRouteInfo<void> {
const LoginRoute({List<_i9.PageRouteInfo>? children}) const LoginRoute({List<_i10.PageRouteInfo>? children})
: super(LoginRoute.name, initialChildren: children); : super(LoginRoute.name, initialChildren: children);
static const String name = 'LoginRoute'; static const String name = 'LoginRoute';
static _i9.PageInfo page = _i9.PageInfo( static _i10.PageInfo page = _i10.PageInfo(
name, name,
builder: (data) { builder: (data) {
return _i9.WrappedRoute(child: const _i3.LoginPage()); return _i10.WrappedRoute(child: const _i3.LoginPage());
}, },
); );
} }
/// generated route for /// generated route for
/// [_i4.MainPage] /// [_i4.MainPage]
class MainRoute extends _i9.PageRouteInfo<void> { class MainRoute extends _i10.PageRouteInfo<void> {
const MainRoute({List<_i9.PageRouteInfo>? children}) const MainRoute({List<_i10.PageRouteInfo>? children})
: super(MainRoute.name, initialChildren: children); : super(MainRoute.name, initialChildren: children);
static const String name = 'MainRoute'; static const String name = 'MainRoute';
static _i9.PageInfo page = _i9.PageInfo( static _i10.PageInfo page = _i10.PageInfo(
name, name,
builder: (data) { builder: (data) {
return const _i4.MainPage(); return const _i4.MainPage();
@ -93,13 +95,13 @@ class MainRoute extends _i9.PageRouteInfo<void> {
/// generated route for /// generated route for
/// [_i5.ReportPage] /// [_i5.ReportPage]
class ReportRoute extends _i9.PageRouteInfo<void> { class ReportRoute extends _i10.PageRouteInfo<void> {
const ReportRoute({List<_i9.PageRouteInfo>? children}) const ReportRoute({List<_i10.PageRouteInfo>? children})
: super(ReportRoute.name, initialChildren: children); : super(ReportRoute.name, initialChildren: children);
static const String name = 'ReportRoute'; static const String name = 'ReportRoute';
static _i9.PageInfo page = _i9.PageInfo( static _i10.PageInfo page = _i10.PageInfo(
name, name,
builder: (data) { builder: (data) {
return const _i5.ReportPage(); return const _i5.ReportPage();
@ -109,13 +111,13 @@ class ReportRoute extends _i9.PageRouteInfo<void> {
/// generated route for /// generated route for
/// [_i6.SettingPage] /// [_i6.SettingPage]
class SettingRoute extends _i9.PageRouteInfo<void> { class SettingRoute extends _i10.PageRouteInfo<void> {
const SettingRoute({List<_i9.PageRouteInfo>? children}) const SettingRoute({List<_i10.PageRouteInfo>? children})
: super(SettingRoute.name, initialChildren: children); : super(SettingRoute.name, initialChildren: children);
static const String name = 'SettingRoute'; static const String name = 'SettingRoute';
static _i9.PageInfo page = _i9.PageInfo( static _i10.PageInfo page = _i10.PageInfo(
name, name,
builder: (data) { builder: (data) {
return const _i6.SettingPage(); return const _i6.SettingPage();
@ -125,13 +127,13 @@ class SettingRoute extends _i9.PageRouteInfo<void> {
/// generated route for /// generated route for
/// [_i7.SplashPage] /// [_i7.SplashPage]
class SplashRoute extends _i9.PageRouteInfo<void> { class SplashRoute extends _i10.PageRouteInfo<void> {
const SplashRoute({List<_i9.PageRouteInfo>? children}) const SplashRoute({List<_i10.PageRouteInfo>? children})
: super(SplashRoute.name, initialChildren: children); : super(SplashRoute.name, initialChildren: children);
static const String name = 'SplashRoute'; static const String name = 'SplashRoute';
static _i9.PageInfo page = _i9.PageInfo( static _i10.PageInfo page = _i10.PageInfo(
name, name,
builder: (data) { builder: (data) {
return const _i7.SplashPage(); return const _i7.SplashPage();
@ -140,17 +142,33 @@ class SplashRoute extends _i9.PageRouteInfo<void> {
} }
/// generated route for /// generated route for
/// [_i8.TablePage] /// [_i8.SyncPage]
class TableRoute extends _i9.PageRouteInfo<void> { class SyncRoute extends _i10.PageRouteInfo<void> {
const TableRoute({List<_i9.PageRouteInfo>? children}) const SyncRoute({List<_i10.PageRouteInfo>? children})
: super(SyncRoute.name, initialChildren: children);
static const String name = 'SyncRoute';
static _i10.PageInfo page = _i10.PageInfo(
name,
builder: (data) {
return const _i8.SyncPage();
},
);
}
/// generated route for
/// [_i9.TablePage]
class TableRoute extends _i10.PageRouteInfo<void> {
const TableRoute({List<_i10.PageRouteInfo>? children})
: super(TableRoute.name, initialChildren: children); : super(TableRoute.name, initialChildren: children);
static const String name = 'TableRoute'; static const String name = 'TableRoute';
static _i9.PageInfo page = _i9.PageInfo( static _i10.PageInfo page = _i10.PageInfo(
name, name,
builder: (data) { builder: (data) {
return const _i8.TablePage(); return const _i9.TablePage();
}, },
); );
} }

View File

@ -10,6 +10,7 @@ import firebase_core
import firebase_crashlytics import firebase_crashlytics
import path_provider_foundation import path_provider_foundation
import shared_preferences_foundation import shared_preferences_foundation
import sqflite_darwin
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
@ -17,4 +18,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTFirebaseCrashlyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCrashlyticsPlugin")) FLTFirebaseCrashlyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCrashlyticsPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
} }

View File

@ -949,6 +949,46 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.10.1" version: "1.10.1"
sqflite:
dependency: "direct main"
description:
name: sqflite
sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03
url: "https://pub.dev"
source: hosted
version: "2.4.2"
sqflite_android:
dependency: transitive
description:
name: sqflite_android
sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88
url: "https://pub.dev"
source: hosted
version: "2.4.2+2"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6"
url: "https://pub.dev"
source: hosted
version: "2.5.6"
sqflite_darwin:
dependency: transitive
description:
name: sqflite_darwin
sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
sqflite_platform_interface:
dependency: transitive
description:
name: sqflite_platform_interface
sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920"
url: "https://pub.dev"
source: hosted
version: "2.4.0"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
@ -981,6 +1021,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.1" version: "1.4.1"
synchronized:
dependency: transitive
description:
name: synchronized
sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0
url: "https://pub.dev"
source: hosted
version: "3.4.0"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: description:

View File

@ -33,6 +33,7 @@ dependencies:
flutter_spinkit: ^5.2.2 flutter_spinkit: ^5.2.2
bloc: ^9.1.0 bloc: ^9.1.0
flutter_bloc: ^9.1.1 flutter_bloc: ^9.1.1
sqflite: ^2.4.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: