feat: implement audit logging and documentation for Tier 1 case management features
This commit is contained in:
parent
814668aa6c
commit
bb364ed7d6
18 changed files with 1118 additions and 1315 deletions
|
|
@ -67,11 +67,12 @@ class AuthState {
|
|||
}
|
||||
|
||||
/// Notifier for authentication state changes
|
||||
class AuthNotifier extends StateNotifier<AuthState> {
|
||||
AuthNotifier() : super(const AuthState.initial());
|
||||
|
||||
class AuthNotifier extends Notifier<AuthState> {
|
||||
final AuthService _authService = AuthService();
|
||||
|
||||
@override
|
||||
AuthState build() => const AuthState.initial();
|
||||
|
||||
/// Check if user is already authenticated (on app launch)
|
||||
Future<void> checkAuthStatus() async {
|
||||
debugPrint('AuthNotifier: Checking auth status...');
|
||||
|
|
@ -190,9 +191,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
|||
}
|
||||
|
||||
/// Provider for authentication state
|
||||
final authProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
|
||||
return AuthNotifier();
|
||||
});
|
||||
final authProvider = NotifierProvider<AuthNotifier, AuthState>(AuthNotifier.new);
|
||||
|
||||
/// Provider for checking if user is authenticated
|
||||
final isAuthenticatedProvider = Provider<bool>((ref) {
|
||||
|
|
|
|||
|
|
@ -4,145 +4,42 @@ import '../config/api_config.dart';
|
|||
import '../data/models/dashboard_data.dart';
|
||||
import '../services/api_service.dart';
|
||||
|
||||
/// Dashboard state
|
||||
class DashboardState {
|
||||
final DashboardData? data;
|
||||
final bool isLoading;
|
||||
final String? error;
|
||||
final DateTime? lastUpdated;
|
||||
|
||||
const DashboardState({
|
||||
this.data,
|
||||
this.isLoading = false,
|
||||
this.error,
|
||||
this.lastUpdated,
|
||||
});
|
||||
|
||||
const DashboardState.initial()
|
||||
: data = null,
|
||||
isLoading = false,
|
||||
error = null,
|
||||
lastUpdated = null;
|
||||
|
||||
DashboardState copyWith({
|
||||
DashboardData? data,
|
||||
bool? isLoading,
|
||||
String? error,
|
||||
DateTime? lastUpdated,
|
||||
bool clearError = false,
|
||||
}) {
|
||||
return DashboardState(
|
||||
data: data ?? this.data,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
error: clearError ? null : (error ?? this.error),
|
||||
lastUpdated: lastUpdated ?? this.lastUpdated,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Dashboard notifier for managing dashboard state
|
||||
class DashboardNotifier extends StateNotifier<DashboardState> {
|
||||
DashboardNotifier() : super(const DashboardState.initial());
|
||||
|
||||
/// Async notifier owning the dashboard fetch.
|
||||
///
|
||||
/// `build()` runs on first watch and seeds the AsyncValue with the result of
|
||||
/// the initial fetch — no separate `fetchDashboard()` call from the screen.
|
||||
/// `refresh()` is the pull-to-refresh / retry entrypoint.
|
||||
class DashboardNotifier extends AsyncNotifier<DashboardData> {
|
||||
final ApiService _apiService = ApiService();
|
||||
|
||||
/// Fetch dashboard data from API
|
||||
Future<void> fetchDashboard({bool forceRefresh = false}) async {
|
||||
// Skip if already loading
|
||||
if (state.isLoading) return;
|
||||
@override
|
||||
Future<DashboardData> build() => _fetch();
|
||||
|
||||
// Skip if we have recent data (less than 1 minute old) and not forcing refresh
|
||||
if (!forceRefresh && state.data != null && state.lastUpdated != null) {
|
||||
final age = DateTime.now().difference(state.lastUpdated!);
|
||||
if (age.inMinutes < 1) {
|
||||
debugPrint(
|
||||
'DashboardNotifier: Using cached data (${age.inSeconds}s old)',
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('DashboardNotifier: Fetching dashboard data...');
|
||||
state = state.copyWith(isLoading: true, clearError: true);
|
||||
|
||||
try {
|
||||
final response = await _apiService.get(ApiConfig.dashboard);
|
||||
|
||||
debugPrint('DashboardNotifier: Response status: ${response.statusCode}');
|
||||
debugPrint('DashboardNotifier: Response success: ${response.success}');
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
debugPrint(
|
||||
'DashboardNotifier: Response keys: ${response.data!.keys.toList()}',
|
||||
);
|
||||
debugPrint(
|
||||
'DashboardNotifier: tasks data type: ${response.data!['tasks']?.runtimeType}',
|
||||
);
|
||||
if (response.data!['tasks'] != null) {
|
||||
final tasksList = response.data!['tasks'] as List<dynamic>;
|
||||
debugPrint('DashboardNotifier: tasks count: ${tasksList.length}');
|
||||
if (tasksList.isNotEmpty) {
|
||||
debugPrint('DashboardNotifier: first task: ${tasksList.first}');
|
||||
}
|
||||
}
|
||||
|
||||
final dashboardData = DashboardData.fromJson(response.data!);
|
||||
state = state.copyWith(
|
||||
data: dashboardData,
|
||||
isLoading: false,
|
||||
lastUpdated: DateTime.now(),
|
||||
);
|
||||
debugPrint('DashboardNotifier: Dashboard data loaded successfully');
|
||||
debugPrint(' - Leads: ${dashboardData.leadsCount}');
|
||||
debugPrint(' - Opportunities: ${dashboardData.opportunitiesCount}');
|
||||
debugPrint(
|
||||
' - Pipeline stages: ${dashboardData.pipelineByStage.length}',
|
||||
);
|
||||
debugPrint(' - Tasks: ${dashboardData.tasks.length}');
|
||||
} else {
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
error: response.message ?? 'Failed to load dashboard data',
|
||||
);
|
||||
debugPrint('DashboardNotifier: API error - ${response.message}');
|
||||
debugPrint('DashboardNotifier: Response data: ${response.data}');
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
debugPrint('DashboardNotifier: Exception - $e');
|
||||
debugPrint('DashboardNotifier: Stack trace - $stackTrace');
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
error: 'Failed to load dashboard: ${e.toString()}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Refresh dashboard data
|
||||
Future<void> refresh() async {
|
||||
await fetchDashboard(forceRefresh: true);
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(_fetch);
|
||||
}
|
||||
|
||||
/// Clear dashboard data (e.g., on logout)
|
||||
void clear() {
|
||||
state = const DashboardState.initial();
|
||||
Future<DashboardData> _fetch() async {
|
||||
debugPrint('DashboardNotifier: Fetching dashboard data...');
|
||||
final response = await _apiService.get(ApiConfig.dashboard);
|
||||
debugPrint('DashboardNotifier: Response status: ${response.statusCode}');
|
||||
if (!response.success || response.data == null) {
|
||||
debugPrint('DashboardNotifier: API error - ${response.message}');
|
||||
throw Exception(response.message ?? 'Failed to load dashboard data');
|
||||
}
|
||||
final dashboardData = DashboardData.fromJson(response.data!);
|
||||
debugPrint(
|
||||
'DashboardNotifier: loaded leads=${dashboardData.leadsCount} '
|
||||
'opps=${dashboardData.opportunitiesCount} '
|
||||
'stages=${dashboardData.pipelineByStage.length} '
|
||||
'tasks=${dashboardData.tasks.length}',
|
||||
);
|
||||
return dashboardData;
|
||||
}
|
||||
}
|
||||
|
||||
/// Dashboard provider
|
||||
final dashboardProvider =
|
||||
StateNotifierProvider<DashboardNotifier, DashboardState>((ref) {
|
||||
return DashboardNotifier();
|
||||
});
|
||||
|
||||
/// Convenience providers for specific dashboard data
|
||||
final dashboardDataProvider = Provider<DashboardData?>((ref) {
|
||||
return ref.watch(dashboardProvider).data;
|
||||
});
|
||||
|
||||
final dashboardLoadingProvider = Provider<bool>((ref) {
|
||||
return ref.watch(dashboardProvider).isLoading;
|
||||
});
|
||||
|
||||
final dashboardErrorProvider = Provider<String?>((ref) {
|
||||
return ref.watch(dashboardProvider).error;
|
||||
});
|
||||
AsyncNotifierProvider<DashboardNotifier, DashboardData>(
|
||||
DashboardNotifier.new,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,45 +3,28 @@ import '../config/api_config.dart';
|
|||
import '../data/models/deal.dart';
|
||||
import '../services/api_service.dart';
|
||||
|
||||
/// Deals list state
|
||||
class DealsState {
|
||||
/// Paginated deals snapshot — wrapped by AsyncValue for loading/error.
|
||||
class DealsListData {
|
||||
final List<Deal> deals;
|
||||
final bool isLoading;
|
||||
final String? error;
|
||||
final int totalCount;
|
||||
final bool hasMore;
|
||||
final int currentOffset;
|
||||
|
||||
const DealsState({
|
||||
const DealsListData({
|
||||
this.deals = const [],
|
||||
this.isLoading = false,
|
||||
this.error,
|
||||
this.totalCount = 0,
|
||||
this.hasMore = true,
|
||||
this.currentOffset = 0,
|
||||
});
|
||||
|
||||
const DealsState.initial()
|
||||
: deals = const [],
|
||||
isLoading = false,
|
||||
error = null,
|
||||
totalCount = 0,
|
||||
hasMore = true,
|
||||
currentOffset = 0;
|
||||
|
||||
DealsState copyWith({
|
||||
DealsListData copyWith({
|
||||
List<Deal>? deals,
|
||||
bool? isLoading,
|
||||
String? error,
|
||||
int? totalCount,
|
||||
bool? hasMore,
|
||||
int? currentOffset,
|
||||
bool clearError = false,
|
||||
}) {
|
||||
return DealsState(
|
||||
return DealsListData(
|
||||
deals: deals ?? this.deals,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
error: clearError ? null : (error ?? this.error),
|
||||
totalCount: totalCount ?? this.totalCount,
|
||||
hasMore: hasMore ?? this.hasMore,
|
||||
currentOffset: currentOffset ?? this.currentOffset,
|
||||
|
|
@ -49,121 +32,104 @@ class DealsState {
|
|||
}
|
||||
}
|
||||
|
||||
/// Deals notifier for managing deals state
|
||||
class DealsNotifier extends StateNotifier<DealsState> {
|
||||
DealsNotifier() : super(const DealsState.initial());
|
||||
|
||||
/// AsyncNotifier driving the deals list + CRUD. The list page is the only
|
||||
/// place that watches this; detail/form screens use the notifier directly
|
||||
/// for one-shot fetches and mutations.
|
||||
class DealsNotifier extends AsyncNotifier<DealsListData> {
|
||||
final ApiService _apiService = ApiService();
|
||||
static const int _pageSize = 20;
|
||||
|
||||
/// Fetch deals from API
|
||||
Future<void> fetchDeals({
|
||||
String? search,
|
||||
String? stage,
|
||||
bool refresh = false,
|
||||
}) async {
|
||||
@override
|
||||
Future<DealsListData> build() => _fetchPage(offset: 0);
|
||||
|
||||
/// Reload the first page (pull-to-refresh / after CRUD).
|
||||
Future<void> refresh({String? search, String? stage}) async {
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(
|
||||
() => _fetchPage(offset: 0, search: search, stage: stage),
|
||||
);
|
||||
}
|
||||
|
||||
/// Append the next page if there's more and we're not already loading.
|
||||
Future<void> loadMore({String? search, String? stage}) async {
|
||||
final current = state.value;
|
||||
if (current == null || !current.hasMore) return;
|
||||
if (state.isLoading) return;
|
||||
|
||||
if (refresh) {
|
||||
state = state.copyWith(currentOffset: 0, hasMore: true, clearError: true);
|
||||
}
|
||||
|
||||
state = state.copyWith(isLoading: true, clearError: true);
|
||||
|
||||
try {
|
||||
// Build query parameters
|
||||
final queryParams = <String, String>{
|
||||
'limit': _pageSize.toString(),
|
||||
'offset': state.currentOffset.toString(),
|
||||
};
|
||||
|
||||
if (search != null && search.isNotEmpty) {
|
||||
queryParams['name'] = search;
|
||||
}
|
||||
if (stage != null && stage.isNotEmpty) {
|
||||
queryParams['stage'] = stage;
|
||||
}
|
||||
|
||||
final url = Uri.parse(
|
||||
ApiConfig.opportunities,
|
||||
).replace(queryParameters: queryParams).toString();
|
||||
final response = await _apiService.get(url);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
final data = response.data!;
|
||||
|
||||
// Parse deals - handle different response formats
|
||||
List<dynamic> dealsList = [];
|
||||
int dealsCount = 0;
|
||||
|
||||
if (data['opportunities'] != null) {
|
||||
dealsList = data['opportunities'] as List<dynamic>? ?? [];
|
||||
dealsCount = data['opportunities_count'] as int? ?? dealsList.length;
|
||||
} else if (data['results'] != null) {
|
||||
dealsList = data['results'] as List<dynamic>? ?? [];
|
||||
dealsCount = data['count'] as int? ?? dealsList.length;
|
||||
}
|
||||
|
||||
final newDeals = <Deal>[];
|
||||
for (final item in dealsList) {
|
||||
try {
|
||||
if (item is Map<String, dynamic>) {
|
||||
newDeals.add(Deal.fromJson(item));
|
||||
}
|
||||
} catch (e) {
|
||||
// Skip invalid deals
|
||||
}
|
||||
}
|
||||
|
||||
// Update state
|
||||
final updatedDeals = refresh ? newDeals : [...state.deals, ...newDeals];
|
||||
|
||||
state = state.copyWith(
|
||||
deals: updatedDeals,
|
||||
isLoading: false,
|
||||
totalCount: dealsCount,
|
||||
hasMore: newDeals.length >= _pageSize,
|
||||
currentOffset: state.currentOffset + newDeals.length,
|
||||
);
|
||||
} else {
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
error: response.message ?? 'Failed to load deals',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
error: 'Failed to load deals: ${e.toString()}',
|
||||
state = await AsyncValue.guard(() async {
|
||||
final next = await _fetchPage(
|
||||
offset: current.currentOffset,
|
||||
search: search,
|
||||
stage: stage,
|
||||
);
|
||||
return current.copyWith(
|
||||
deals: [...current.deals, ...next.deals],
|
||||
totalCount: next.totalCount,
|
||||
hasMore: next.hasMore,
|
||||
currentOffset: next.currentOffset,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<DealsListData> _fetchPage({
|
||||
required int offset,
|
||||
String? search,
|
||||
String? stage,
|
||||
}) async {
|
||||
final queryParams = <String, String>{
|
||||
'limit': _pageSize.toString(),
|
||||
'offset': offset.toString(),
|
||||
};
|
||||
if (search != null && search.isNotEmpty) queryParams['name'] = search;
|
||||
if (stage != null && stage.isNotEmpty) queryParams['stage'] = stage;
|
||||
|
||||
final url = Uri.parse(
|
||||
ApiConfig.opportunities,
|
||||
).replace(queryParameters: queryParams).toString();
|
||||
final response = await _apiService.get(url);
|
||||
|
||||
if (!response.success || response.data == null) {
|
||||
throw Exception(response.message ?? 'Failed to load deals');
|
||||
}
|
||||
|
||||
final data = response.data!;
|
||||
List<dynamic> dealsList = [];
|
||||
int dealsCount = 0;
|
||||
|
||||
if (data['opportunities'] != null) {
|
||||
dealsList = data['opportunities'] as List<dynamic>? ?? [];
|
||||
dealsCount = data['opportunities_count'] as int? ?? dealsList.length;
|
||||
} else if (data['results'] != null) {
|
||||
dealsList = data['results'] as List<dynamic>? ?? [];
|
||||
dealsCount = data['count'] as int? ?? dealsList.length;
|
||||
}
|
||||
|
||||
final newDeals = <Deal>[];
|
||||
for (final item in dealsList) {
|
||||
try {
|
||||
if (item is Map<String, dynamic>) {
|
||||
newDeals.add(Deal.fromJson(item));
|
||||
}
|
||||
} catch (_) {
|
||||
// Skip invalid deals
|
||||
}
|
||||
}
|
||||
|
||||
return DealsListData(
|
||||
deals: newDeals,
|
||||
totalCount: dealsCount,
|
||||
hasMore: newDeals.length >= _pageSize,
|
||||
currentOffset: offset + newDeals.length,
|
||||
);
|
||||
}
|
||||
|
||||
/// Refresh deals (reset and fetch first page)
|
||||
Future<void> refresh() async {
|
||||
state = const DealsState.initial();
|
||||
await fetchDeals(refresh: true);
|
||||
}
|
||||
|
||||
/// Load more deals
|
||||
Future<void> loadMore() async {
|
||||
if (!state.hasMore || state.isLoading) return;
|
||||
await fetchDeals();
|
||||
}
|
||||
|
||||
/// Clear deals data
|
||||
void clear() {
|
||||
state = const DealsState.initial();
|
||||
}
|
||||
|
||||
/// Get a single deal by ID
|
||||
/// Fetch a single deal from the API (detail/form screens).
|
||||
Future<Deal?> getDeal(String id) async {
|
||||
try {
|
||||
final url = '${ApiConfig.opportunities}$id/';
|
||||
final response = await _apiService.get(url);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
// API returns opportunity wrapped in 'opportunity_obj' key
|
||||
final opportunityData =
|
||||
response.data!['opportunity_obj'] as Map<String, dynamic>?;
|
||||
if (opportunityData != null) {
|
||||
|
|
@ -176,7 +142,7 @@ class DealsNotifier extends StateNotifier<DealsState> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Create a new deal
|
||||
/// Create a new deal — list refreshes on success.
|
||||
Future<({bool success, String? error, Deal? deal})> createDeal(
|
||||
Deal deal,
|
||||
) async {
|
||||
|
|
@ -187,16 +153,13 @@ class DealsNotifier extends StateNotifier<DealsState> {
|
|||
);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
// API returns {"error": false, "message": "..."} on success, not the deal object
|
||||
final isError = response.data!['error'] as bool? ?? true;
|
||||
if (!isError) {
|
||||
// Refresh the deals list to get the new deal
|
||||
await fetchDeals(refresh: true);
|
||||
await refresh();
|
||||
return (success: true, error: null, deal: null);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse error message
|
||||
String errorMsg = response.message ?? 'Failed to create deal';
|
||||
if (response.data != null && response.data!['errors'] != null) {
|
||||
final errors = response.data!['errors'] as Map<String, dynamic>;
|
||||
|
|
@ -214,7 +177,7 @@ class DealsNotifier extends StateNotifier<DealsState> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Update an existing deal
|
||||
/// Update an existing deal — list refreshes on success.
|
||||
Future<({bool success, String? error, Deal? deal})> updateDeal(
|
||||
String id,
|
||||
Deal deal,
|
||||
|
|
@ -224,16 +187,13 @@ class DealsNotifier extends StateNotifier<DealsState> {
|
|||
final response = await _apiService.put(url, deal.toJson());
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
// API returns {"error": false, "message": "..."} on success, not the deal object
|
||||
final isError = response.data!['error'] as bool? ?? true;
|
||||
if (!isError) {
|
||||
// Refresh the deals list to get updated data
|
||||
await fetchDeals(refresh: true);
|
||||
await refresh();
|
||||
return (success: true, error: null, deal: null);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse error message
|
||||
String errorMsg = response.message ?? 'Failed to update deal';
|
||||
if (response.data != null && response.data!['errors'] != null) {
|
||||
final errors = response.data!['errors'] as Map<String, dynamic>;
|
||||
|
|
@ -251,7 +211,7 @@ class DealsNotifier extends StateNotifier<DealsState> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Update deal stage (quick action)
|
||||
/// Quick stage change — optimistic local update for snappy UX.
|
||||
Future<({bool success, String? error})> updateDealStage(
|
||||
String id,
|
||||
DealStage stage,
|
||||
|
|
@ -264,21 +224,22 @@ class DealsNotifier extends StateNotifier<DealsState> {
|
|||
});
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
// API returns {"error": false, "message": "..."} on success
|
||||
final isError = response.data!['error'] as bool? ?? true;
|
||||
if (!isError) {
|
||||
// Update the deal stage locally without full refresh for better UX
|
||||
final updatedDeals = state.deals.map((d) {
|
||||
if (d.id == id) {
|
||||
return d.copyWith(
|
||||
stage: stage,
|
||||
probability: stage.defaultProbability,
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
return d;
|
||||
}).toList();
|
||||
state = state.copyWith(deals: updatedDeals);
|
||||
final current = state.value;
|
||||
if (current != null) {
|
||||
final updatedDeals = current.deals.map((d) {
|
||||
if (d.id == id) {
|
||||
return d.copyWith(
|
||||
stage: stage,
|
||||
probability: stage.defaultProbability,
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
return d;
|
||||
}).toList();
|
||||
state = AsyncValue.data(current.copyWith(deals: updatedDeals));
|
||||
}
|
||||
return (success: true, error: null);
|
||||
}
|
||||
}
|
||||
|
|
@ -292,19 +253,22 @@ class DealsNotifier extends StateNotifier<DealsState> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Delete a deal
|
||||
/// Delete a deal — local state mutation (no full refresh).
|
||||
Future<({bool success, String? error})> deleteDeal(String id) async {
|
||||
try {
|
||||
final url = '${ApiConfig.opportunities}$id/';
|
||||
final response = await _apiService.delete(url);
|
||||
|
||||
if (response.success) {
|
||||
// Remove from the list
|
||||
final updatedDeals = state.deals.where((d) => d.id != id).toList();
|
||||
state = state.copyWith(
|
||||
deals: updatedDeals,
|
||||
totalCount: state.totalCount - 1,
|
||||
);
|
||||
final current = state.value;
|
||||
if (current != null) {
|
||||
state = AsyncValue.data(
|
||||
current.copyWith(
|
||||
deals: current.deals.where((d) => d.id != id).toList(),
|
||||
totalCount: current.totalCount - 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
return (success: true, error: null);
|
||||
}
|
||||
|
||||
|
|
@ -316,25 +280,16 @@ class DealsNotifier extends StateNotifier<DealsState> {
|
|||
return (success: false, error: 'Failed to delete deal: ${e.toString()}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Get deal from current state by ID
|
||||
Deal? getDealFromState(String id) {
|
||||
try {
|
||||
return state.deals.firstWhere((d) => d.id == id);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Deals provider
|
||||
final dealsProvider = StateNotifierProvider<DealsNotifier, DealsState>((ref) {
|
||||
return DealsNotifier();
|
||||
});
|
||||
final dealsProvider = AsyncNotifierProvider<DealsNotifier, DealsListData>(
|
||||
DealsNotifier.new,
|
||||
);
|
||||
|
||||
/// Convenience providers
|
||||
/// Convenience providers — read from the AsyncValue so screen code stays the
|
||||
/// same shape as before the riverpod 3 migration.
|
||||
final dealsListProvider = Provider<List<Deal>>((ref) {
|
||||
return ref.watch(dealsProvider).deals;
|
||||
return ref.watch(dealsProvider).value?.deals ?? const [];
|
||||
});
|
||||
|
||||
final dealsLoadingProvider = Provider<bool>((ref) {
|
||||
|
|
@ -342,22 +297,20 @@ final dealsLoadingProvider = Provider<bool>((ref) {
|
|||
});
|
||||
|
||||
final dealsErrorProvider = Provider<String?>((ref) {
|
||||
return ref.watch(dealsProvider).error;
|
||||
return ref.watch(dealsProvider).error?.toString();
|
||||
});
|
||||
|
||||
/// Grouped deals by stage providers
|
||||
/// Grouped deals by stage — derived.
|
||||
final dealsByStageProvider = Provider<Map<DealStage, List<Deal>>>((ref) {
|
||||
final deals = ref.watch(dealsListProvider);
|
||||
final Map<DealStage, List<Deal>> grouped = {};
|
||||
|
||||
for (final stage in DealStage.values) {
|
||||
grouped[stage] = deals.where((deal) => deal.stage == stage).toList();
|
||||
}
|
||||
|
||||
return grouped;
|
||||
});
|
||||
|
||||
/// Pipeline value (active deals only)
|
||||
/// Pipeline value (active deals only).
|
||||
final pipelineValueProvider = Provider<double>((ref) {
|
||||
final deals = ref.watch(dealsListProvider);
|
||||
return deals
|
||||
|
|
@ -365,7 +318,7 @@ final pipelineValueProvider = Provider<double>((ref) {
|
|||
.fold<double>(0, (sum, deal) => sum + deal.value);
|
||||
});
|
||||
|
||||
/// Active deals count
|
||||
/// Active deals count.
|
||||
final activeDealsCountProvider = Provider<int>((ref) {
|
||||
final deals = ref.watch(dealsListProvider);
|
||||
return deals.where((d) => !d.stage.isClosed).length;
|
||||
|
|
|
|||
|
|
@ -4,45 +4,28 @@ import '../data/models/lead.dart';
|
|||
import '../data/models/comment.dart';
|
||||
import '../services/api_service.dart';
|
||||
|
||||
/// Leads list state
|
||||
class LeadsState {
|
||||
/// Paginated leads snapshot — wrapped by AsyncValue.
|
||||
class LeadsListData {
|
||||
final List<Lead> leads;
|
||||
final bool isLoading;
|
||||
final String? error;
|
||||
final int totalCount;
|
||||
final bool hasMore;
|
||||
final int currentOffset;
|
||||
|
||||
const LeadsState({
|
||||
const LeadsListData({
|
||||
this.leads = const [],
|
||||
this.isLoading = false,
|
||||
this.error,
|
||||
this.totalCount = 0,
|
||||
this.hasMore = true,
|
||||
this.currentOffset = 0,
|
||||
});
|
||||
|
||||
const LeadsState.initial()
|
||||
: leads = const [],
|
||||
isLoading = false,
|
||||
error = null,
|
||||
totalCount = 0,
|
||||
hasMore = true,
|
||||
currentOffset = 0;
|
||||
|
||||
LeadsState copyWith({
|
||||
LeadsListData copyWith({
|
||||
List<Lead>? leads,
|
||||
bool? isLoading,
|
||||
String? error,
|
||||
int? totalCount,
|
||||
bool? hasMore,
|
||||
int? currentOffset,
|
||||
bool clearError = false,
|
||||
}) {
|
||||
return LeadsState(
|
||||
return LeadsListData(
|
||||
leads: leads ?? this.leads,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
error: clearError ? null : (error ?? this.error),
|
||||
totalCount: totalCount ?? this.totalCount,
|
||||
hasMore: hasMore ?? this.hasMore,
|
||||
currentOffset: currentOffset ?? this.currentOffset,
|
||||
|
|
@ -50,14 +33,102 @@ class LeadsState {
|
|||
}
|
||||
}
|
||||
|
||||
/// Leads notifier for managing leads state
|
||||
class LeadsNotifier extends StateNotifier<LeadsState> {
|
||||
LeadsNotifier() : super(const LeadsState.initial());
|
||||
|
||||
class LeadsNotifier extends AsyncNotifier<LeadsListData> {
|
||||
final ApiService _apiService = ApiService();
|
||||
static const int _pageSize = 20;
|
||||
|
||||
/// Get a single lead by ID
|
||||
@override
|
||||
Future<LeadsListData> build() => _fetchPage(offset: 0);
|
||||
|
||||
Future<void> refresh({String? search, String? status, String? source}) async {
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(
|
||||
() => _fetchPage(
|
||||
offset: 0,
|
||||
search: search,
|
||||
status: status,
|
||||
source: source,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> loadMore({
|
||||
String? search,
|
||||
String? status,
|
||||
String? source,
|
||||
}) async {
|
||||
final current = state.value;
|
||||
if (current == null || !current.hasMore) return;
|
||||
if (state.isLoading) return;
|
||||
|
||||
state = await AsyncValue.guard(() async {
|
||||
final next = await _fetchPage(
|
||||
offset: current.currentOffset,
|
||||
search: search,
|
||||
status: status,
|
||||
source: source,
|
||||
);
|
||||
return current.copyWith(
|
||||
leads: [...current.leads, ...next.leads],
|
||||
totalCount: next.totalCount,
|
||||
hasMore: next.hasMore,
|
||||
currentOffset: next.currentOffset,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<LeadsListData> _fetchPage({
|
||||
required int offset,
|
||||
String? search,
|
||||
String? status,
|
||||
String? source,
|
||||
}) async {
|
||||
final queryParams = <String, String>{
|
||||
'limit': _pageSize.toString(),
|
||||
'offset': offset.toString(),
|
||||
};
|
||||
if (search != null && search.isNotEmpty) queryParams['search'] = search;
|
||||
if (status != null && status.isNotEmpty) queryParams['status'] = status;
|
||||
if (source != null && source.isNotEmpty) queryParams['source'] = source;
|
||||
|
||||
final url = Uri.parse(
|
||||
ApiConfig.leads,
|
||||
).replace(queryParameters: queryParams).toString();
|
||||
final response = await _apiService.get(url);
|
||||
|
||||
if (!response.success || response.data == null) {
|
||||
throw Exception(response.message ?? 'Failed to load leads');
|
||||
}
|
||||
|
||||
final data = response.data!;
|
||||
|
||||
// Backend returns open_leads + close_leads in two sub-objects.
|
||||
final openLeadsData = data['open_leads'] as Map<String, dynamic>?;
|
||||
final openLeadsList =
|
||||
openLeadsData?['open_leads'] as List<dynamic>? ?? [];
|
||||
final openLeadsCount = openLeadsData?['leads_count'] as int? ?? 0;
|
||||
|
||||
final closeLeadsData = data['close_leads'] as Map<String, dynamic>?;
|
||||
final closeLeadsList =
|
||||
closeLeadsData?['close_leads'] as List<dynamic>? ?? [];
|
||||
|
||||
final allLeadsList = [...openLeadsList, ...closeLeadsList];
|
||||
final newLeads = allLeadsList
|
||||
.map((json) => Lead.fromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
final totalCount =
|
||||
openLeadsCount + (closeLeadsData?['leads_count'] as int? ?? 0);
|
||||
|
||||
return LeadsListData(
|
||||
leads: newLeads,
|
||||
totalCount: totalCount,
|
||||
hasMore: newLeads.length >= _pageSize,
|
||||
currentOffset: offset + newLeads.length,
|
||||
);
|
||||
}
|
||||
|
||||
/// Fetch a single lead from the API (with comments).
|
||||
Future<Lead?> getLeadById(String id) async {
|
||||
try {
|
||||
final url = '${ApiConfig.leads}$id/';
|
||||
|
|
@ -68,7 +139,6 @@ class LeadsNotifier extends StateNotifier<LeadsState> {
|
|||
if (leadData != null) {
|
||||
final lead = Lead.fromJson(leadData);
|
||||
|
||||
// Parse comments from separate field in response
|
||||
final commentsData = response.data!['comments'] as List<dynamic>?;
|
||||
if (commentsData != null && commentsData.isNotEmpty) {
|
||||
final comments = commentsData
|
||||
|
|
@ -76,7 +146,6 @@ class LeadsNotifier extends StateNotifier<LeadsState> {
|
|||
.toList();
|
||||
return lead.copyWith(comments: comments);
|
||||
}
|
||||
|
||||
return lead;
|
||||
}
|
||||
}
|
||||
|
|
@ -86,22 +155,18 @@ class LeadsNotifier extends StateNotifier<LeadsState> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Create a new lead
|
||||
Future<ApiResponse<Map<String, dynamic>>> createLead(
|
||||
Map<String, dynamic> leadData,
|
||||
) async {
|
||||
try {
|
||||
final response = await _apiService.post(ApiConfig.leads, leadData);
|
||||
if (response.success) {
|
||||
await refresh();
|
||||
}
|
||||
if (response.success) await refresh();
|
||||
return response;
|
||||
} catch (e) {
|
||||
return ApiResponse(success: false, message: e.toString(), statusCode: 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Update an existing lead
|
||||
Future<ApiResponse<Map<String, dynamic>>> updateLead(
|
||||
String id,
|
||||
Map<String, dynamic> leadData,
|
||||
|
|
@ -109,16 +174,14 @@ class LeadsNotifier extends StateNotifier<LeadsState> {
|
|||
try {
|
||||
final url = '${ApiConfig.leads}$id/';
|
||||
final response = await _apiService.put(url, leadData);
|
||||
if (response.success) {
|
||||
await refresh();
|
||||
}
|
||||
if (response.success) await refresh();
|
||||
return response;
|
||||
} catch (e) {
|
||||
return ApiResponse(success: false, message: e.toString(), statusCode: 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Update lead status (quick action)
|
||||
/// Quick status change — optimistic local update.
|
||||
Future<ApiResponse<Map<String, dynamic>>> updateLeadStatus(
|
||||
String id,
|
||||
LeadStatus status,
|
||||
|
|
@ -128,32 +191,37 @@ class LeadsNotifier extends StateNotifier<LeadsState> {
|
|||
final response = await _apiService.patch(url, {'status': status.value});
|
||||
|
||||
if (response.success) {
|
||||
// Update local state
|
||||
final updatedLeads = state.leads.map((l) {
|
||||
if (l.id == id) {
|
||||
return l.copyWith(status: status);
|
||||
}
|
||||
return l;
|
||||
}).toList();
|
||||
state = state.copyWith(leads: updatedLeads);
|
||||
final current = state.value;
|
||||
if (current != null) {
|
||||
final updatedLeads = current.leads.map((l) {
|
||||
if (l.id == id) return l.copyWith(status: status);
|
||||
return l;
|
||||
}).toList();
|
||||
state = AsyncValue.data(current.copyWith(leads: updatedLeads));
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (e) {
|
||||
return ApiResponse(success: false, message: e.toString(), statusCode: 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a lead
|
||||
Future<ApiResponse<Map<String, dynamic>>> deleteLead(String id) async {
|
||||
try {
|
||||
final url = '${ApiConfig.leads}$id/';
|
||||
final response = await _apiService.delete(url);
|
||||
if (response.success) {
|
||||
state = state.copyWith(
|
||||
leads: state.leads.where((l) => l.id != id).toList(),
|
||||
totalCount: state.totalCount > 0 ? state.totalCount - 1 : 0,
|
||||
);
|
||||
final current = state.value;
|
||||
if (current != null) {
|
||||
state = AsyncValue.data(
|
||||
current.copyWith(
|
||||
leads: current.leads.where((l) => l.id != id).toList(),
|
||||
totalCount: current.totalCount > 0
|
||||
? current.totalCount - 1
|
||||
: 0,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return response;
|
||||
} catch (e) {
|
||||
|
|
@ -161,142 +229,37 @@ class LeadsNotifier extends StateNotifier<LeadsState> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Add a comment to a lead
|
||||
Future<ApiResponse<Map<String, dynamic>>> addComment(
|
||||
String leadId,
|
||||
String comment,
|
||||
) async {
|
||||
try {
|
||||
final url = '${ApiConfig.leads}$leadId/';
|
||||
final response = await _apiService.post(url, {'comment': comment});
|
||||
return response;
|
||||
return await _apiService.post(url, {'comment': comment});
|
||||
} catch (e) {
|
||||
return ApiResponse(success: false, message: e.toString(), statusCode: 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a comment from a lead
|
||||
Future<ApiResponse<Map<String, dynamic>>> deleteComment(
|
||||
String commentId,
|
||||
) async {
|
||||
try {
|
||||
final url = ApiConfig.leadComment(commentId);
|
||||
final response = await _apiService.delete(url);
|
||||
return response;
|
||||
return await _apiService.delete(url);
|
||||
} catch (e) {
|
||||
return ApiResponse(success: false, message: e.toString(), statusCode: 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch leads from API
|
||||
Future<void> fetchLeads({
|
||||
String? search,
|
||||
String? status,
|
||||
String? source,
|
||||
bool refresh = false,
|
||||
}) async {
|
||||
if (state.isLoading) return;
|
||||
|
||||
if (refresh) {
|
||||
state = state.copyWith(currentOffset: 0, hasMore: true, clearError: true);
|
||||
}
|
||||
|
||||
state = state.copyWith(isLoading: true, clearError: true);
|
||||
|
||||
try {
|
||||
// Build query parameters
|
||||
final queryParams = <String, String>{
|
||||
'limit': _pageSize.toString(),
|
||||
'offset': state.currentOffset.toString(),
|
||||
};
|
||||
|
||||
if (search != null && search.isNotEmpty) {
|
||||
queryParams['search'] = search;
|
||||
}
|
||||
if (status != null && status.isNotEmpty) {
|
||||
queryParams['status'] = status;
|
||||
}
|
||||
if (source != null && source.isNotEmpty) {
|
||||
queryParams['source'] = source;
|
||||
}
|
||||
|
||||
final url = Uri.parse(
|
||||
ApiConfig.leads,
|
||||
).replace(queryParameters: queryParams).toString();
|
||||
final response = await _apiService.get(url);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
final data = response.data!;
|
||||
|
||||
// Parse open leads
|
||||
final openLeadsData = data['open_leads'] as Map<String, dynamic>?;
|
||||
final openLeadsList =
|
||||
openLeadsData?['open_leads'] as List<dynamic>? ?? [];
|
||||
final openLeadsCount = openLeadsData?['leads_count'] as int? ?? 0;
|
||||
|
||||
// Parse close leads
|
||||
final closeLeadsData = data['close_leads'] as Map<String, dynamic>?;
|
||||
final closeLeadsList =
|
||||
closeLeadsData?['close_leads'] as List<dynamic>? ?? [];
|
||||
|
||||
// Combine all leads
|
||||
final allLeadsList = [...openLeadsList, ...closeLeadsList];
|
||||
final newLeads = allLeadsList
|
||||
.map((json) => Lead.fromJson(json as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
// Update state
|
||||
final updatedLeads = refresh ? newLeads : [...state.leads, ...newLeads];
|
||||
final totalCount =
|
||||
openLeadsCount + (closeLeadsData?['leads_count'] as int? ?? 0);
|
||||
|
||||
state = state.copyWith(
|
||||
leads: updatedLeads,
|
||||
isLoading: false,
|
||||
totalCount: totalCount,
|
||||
hasMore: newLeads.length >= _pageSize,
|
||||
currentOffset: state.currentOffset + newLeads.length,
|
||||
);
|
||||
} else {
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
error: response.message ?? 'Failed to load leads',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
error: 'Failed to load leads: ${e.toString()}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Refresh leads (reset and fetch first page)
|
||||
Future<void> refresh() async {
|
||||
state = const LeadsState.initial();
|
||||
await fetchLeads(refresh: true);
|
||||
}
|
||||
|
||||
/// Load more leads
|
||||
Future<void> loadMore() async {
|
||||
if (!state.hasMore || state.isLoading) return;
|
||||
await fetchLeads();
|
||||
}
|
||||
|
||||
/// Clear leads data
|
||||
void clear() {
|
||||
state = const LeadsState.initial();
|
||||
}
|
||||
}
|
||||
|
||||
/// Leads provider
|
||||
final leadsProvider = StateNotifierProvider<LeadsNotifier, LeadsState>((ref) {
|
||||
return LeadsNotifier();
|
||||
});
|
||||
final leadsProvider = AsyncNotifierProvider<LeadsNotifier, LeadsListData>(
|
||||
LeadsNotifier.new,
|
||||
);
|
||||
|
||||
/// Convenience providers
|
||||
/// Convenience providers — read from the AsyncValue.
|
||||
final leadsListProvider = Provider<List<Lead>>((ref) {
|
||||
return ref.watch(leadsProvider).leads;
|
||||
return ref.watch(leadsProvider).value?.leads ?? const [];
|
||||
});
|
||||
|
||||
final leadsLoadingProvider = Provider<bool>((ref) {
|
||||
|
|
@ -304,5 +267,5 @@ final leadsLoadingProvider = Provider<bool>((ref) {
|
|||
});
|
||||
|
||||
final leadsErrorProvider = Provider<String?>((ref) {
|
||||
return ref.watch(leadsProvider).error;
|
||||
return ref.watch(leadsProvider).error?.toString();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,341 +3,161 @@ import '../config/api_config.dart';
|
|||
import '../data/models/lookup_models.dart';
|
||||
import '../services/api_service.dart';
|
||||
|
||||
/// State for lookup data (accounts, contacts, users, tags)
|
||||
class LookupState {
|
||||
final List<AccountLookup> accounts;
|
||||
final List<ContactLookup> contacts;
|
||||
final List<UserLookup> users;
|
||||
final List<TagLookup> tags;
|
||||
final bool isLoadingAccounts;
|
||||
final bool isLoadingContacts;
|
||||
final bool isLoadingUsers;
|
||||
final bool isLoadingTags;
|
||||
final String? error;
|
||||
/// Lookup providers — one AsyncNotifier per resource so the four fetches
|
||||
/// (accounts / contacts / users / tags) cache, refresh and surface
|
||||
/// loading/error independently.
|
||||
///
|
||||
/// Forms watch the convenience `Provider<List<X>>` wrappers below for the
|
||||
/// resolved list, and read the underlying `AsyncNotifierProvider`s directly
|
||||
/// when they need the loading flag.
|
||||
|
||||
const LookupState({
|
||||
this.accounts = const [],
|
||||
this.contacts = const [],
|
||||
this.users = const [],
|
||||
this.tags = const [],
|
||||
this.isLoadingAccounts = false,
|
||||
this.isLoadingContacts = false,
|
||||
this.isLoadingUsers = false,
|
||||
this.isLoadingTags = false,
|
||||
this.error,
|
||||
});
|
||||
final ApiService _apiService = ApiService();
|
||||
|
||||
const LookupState.initial()
|
||||
: accounts = const [],
|
||||
contacts = const [],
|
||||
users = const [],
|
||||
tags = const [],
|
||||
isLoadingAccounts = false,
|
||||
isLoadingContacts = false,
|
||||
isLoadingUsers = false,
|
||||
isLoadingTags = false,
|
||||
error = null;
|
||||
/// Accounts
|
||||
class AccountsLookupNotifier extends AsyncNotifier<List<AccountLookup>> {
|
||||
@override
|
||||
Future<List<AccountLookup>> build() => _fetch();
|
||||
|
||||
bool get isLoading =>
|
||||
isLoadingAccounts || isLoadingContacts || isLoadingUsers || isLoadingTags;
|
||||
Future<void> refresh() async {
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(_fetch);
|
||||
}
|
||||
|
||||
LookupState copyWith({
|
||||
List<AccountLookup>? accounts,
|
||||
List<ContactLookup>? contacts,
|
||||
List<UserLookup>? users,
|
||||
List<TagLookup>? tags,
|
||||
bool? isLoadingAccounts,
|
||||
bool? isLoadingContacts,
|
||||
bool? isLoadingUsers,
|
||||
bool? isLoadingTags,
|
||||
String? error,
|
||||
bool clearError = false,
|
||||
}) {
|
||||
return LookupState(
|
||||
accounts: accounts ?? this.accounts,
|
||||
contacts: contacts ?? this.contacts,
|
||||
users: users ?? this.users,
|
||||
tags: tags ?? this.tags,
|
||||
isLoadingAccounts: isLoadingAccounts ?? this.isLoadingAccounts,
|
||||
isLoadingContacts: isLoadingContacts ?? this.isLoadingContacts,
|
||||
isLoadingUsers: isLoadingUsers ?? this.isLoadingUsers,
|
||||
isLoadingTags: isLoadingTags ?? this.isLoadingTags,
|
||||
error: clearError ? null : (error ?? this.error),
|
||||
Future<List<AccountLookup>> _fetch() async {
|
||||
final response = await _apiService.get(ApiConfig.accounts);
|
||||
if (!response.success || response.data == null) {
|
||||
throw Exception(response.message ?? 'Failed to load accounts');
|
||||
}
|
||||
final data = response.data!;
|
||||
List<dynamic> list = [];
|
||||
if (data['accounts'] != null) {
|
||||
list = data['accounts'] as List<dynamic>? ?? [];
|
||||
} else if (data['results'] != null) {
|
||||
list = data['results'] as List<dynamic>? ?? [];
|
||||
}
|
||||
return list
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(AccountLookup.fromJson)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
final accountsLookupProvider =
|
||||
AsyncNotifierProvider<AccountsLookupNotifier, List<AccountLookup>>(
|
||||
AccountsLookupNotifier.new,
|
||||
);
|
||||
|
||||
/// Contacts
|
||||
class ContactsLookupNotifier extends AsyncNotifier<List<ContactLookup>> {
|
||||
@override
|
||||
Future<List<ContactLookup>> build() => _fetch();
|
||||
|
||||
Future<void> refresh() async {
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(_fetch);
|
||||
}
|
||||
|
||||
Future<List<ContactLookup>> _fetch() async {
|
||||
final response = await _apiService.get(ApiConfig.contacts);
|
||||
if (!response.success || response.data == null) {
|
||||
throw Exception(response.message ?? 'Failed to load contacts');
|
||||
}
|
||||
final data = response.data!;
|
||||
List<dynamic> list = [];
|
||||
if (data['contacts'] != null) {
|
||||
list = data['contacts'] as List<dynamic>? ?? [];
|
||||
} else if (data['results'] != null) {
|
||||
list = data['results'] as List<dynamic>? ?? [];
|
||||
}
|
||||
return list
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(ContactLookup.fromJson)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
/// Notifier for managing lookup data
|
||||
class LookupNotifier extends StateNotifier<LookupState> {
|
||||
LookupNotifier() : super(const LookupState.initial());
|
||||
final contactsLookupProvider =
|
||||
AsyncNotifierProvider<ContactsLookupNotifier, List<ContactLookup>>(
|
||||
ContactsLookupNotifier.new,
|
||||
);
|
||||
|
||||
final ApiService _apiService = ApiService();
|
||||
/// Users (active profiles)
|
||||
class UsersLookupNotifier extends AsyncNotifier<List<UserLookup>> {
|
||||
@override
|
||||
Future<List<UserLookup>> build() => _fetch();
|
||||
|
||||
/// Fetch all lookup data
|
||||
Future<void> fetchAll() async {
|
||||
await Future.wait([
|
||||
fetchAccounts(),
|
||||
fetchContacts(),
|
||||
fetchUsers(),
|
||||
fetchTags(),
|
||||
]);
|
||||
Future<void> refresh() async {
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(_fetch);
|
||||
}
|
||||
|
||||
/// Fetch accounts
|
||||
Future<void> fetchAccounts() async {
|
||||
if (state.isLoadingAccounts) return;
|
||||
|
||||
state = state.copyWith(isLoadingAccounts: true, clearError: true);
|
||||
|
||||
try {
|
||||
final response = await _apiService.get(ApiConfig.accounts);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
final data = response.data!;
|
||||
|
||||
// Parse accounts - handle different response formats
|
||||
List<dynamic> accountsList = [];
|
||||
if (data['accounts'] != null) {
|
||||
accountsList = data['accounts'] as List<dynamic>? ?? [];
|
||||
} else if (data['results'] != null) {
|
||||
accountsList = data['results'] as List<dynamic>? ?? [];
|
||||
}
|
||||
|
||||
final accounts = <AccountLookup>[];
|
||||
for (final item in accountsList) {
|
||||
if (item is Map<String, dynamic>) {
|
||||
accounts.add(AccountLookup.fromJson(item));
|
||||
}
|
||||
}
|
||||
|
||||
state = state.copyWith(accounts: accounts, isLoadingAccounts: false);
|
||||
} else {
|
||||
state = state.copyWith(
|
||||
isLoadingAccounts: false,
|
||||
error: response.message ?? 'Failed to load accounts',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
state = state.copyWith(
|
||||
isLoadingAccounts: false,
|
||||
error: 'Failed to load accounts: ${e.toString()}',
|
||||
);
|
||||
Future<List<UserLookup>> _fetch() async {
|
||||
final response = await _apiService.get(ApiConfig.teamsAndUsers);
|
||||
if (!response.success || response.data == null) {
|
||||
throw Exception(response.message ?? 'Failed to load users');
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch contacts
|
||||
Future<void> fetchContacts() async {
|
||||
if (state.isLoadingContacts) return;
|
||||
|
||||
state = state.copyWith(isLoadingContacts: true, clearError: true);
|
||||
|
||||
try {
|
||||
final response = await _apiService.get(ApiConfig.contacts);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
final data = response.data!;
|
||||
|
||||
// Parse contacts - handle different response formats
|
||||
List<dynamic> contactsList = [];
|
||||
if (data['contacts'] != null) {
|
||||
contactsList = data['contacts'] as List<dynamic>? ?? [];
|
||||
} else if (data['results'] != null) {
|
||||
contactsList = data['results'] as List<dynamic>? ?? [];
|
||||
}
|
||||
|
||||
final contacts = <ContactLookup>[];
|
||||
for (final item in contactsList) {
|
||||
if (item is Map<String, dynamic>) {
|
||||
contacts.add(ContactLookup.fromJson(item));
|
||||
}
|
||||
}
|
||||
|
||||
state = state.copyWith(contacts: contacts, isLoadingContacts: false);
|
||||
} else {
|
||||
state = state.copyWith(
|
||||
isLoadingContacts: false,
|
||||
error: response.message ?? 'Failed to load contacts',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
state = state.copyWith(
|
||||
isLoadingContacts: false,
|
||||
error: 'Failed to load contacts: ${e.toString()}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch users (profiles for assignment)
|
||||
Future<void> fetchUsers() async {
|
||||
if (state.isLoadingUsers) return;
|
||||
|
||||
state = state.copyWith(isLoadingUsers: true, clearError: true);
|
||||
|
||||
try {
|
||||
final response = await _apiService.get(ApiConfig.teamsAndUsers);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
final data = response.data!;
|
||||
|
||||
// Parse profiles from the response
|
||||
List<dynamic> profilesList = [];
|
||||
if (data['profiles'] != null) {
|
||||
profilesList = data['profiles'] as List<dynamic>? ?? [];
|
||||
}
|
||||
|
||||
final users = <UserLookup>[];
|
||||
for (final item in profilesList) {
|
||||
if (item is Map<String, dynamic>) {
|
||||
final user = UserLookup.fromJson(item);
|
||||
// Only include active users
|
||||
if (user.isActive) {
|
||||
users.add(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state = state.copyWith(users: users, isLoadingUsers: false);
|
||||
} else {
|
||||
state = state.copyWith(
|
||||
isLoadingUsers: false,
|
||||
error: response.message ?? 'Failed to load users',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
state = state.copyWith(
|
||||
isLoadingUsers: false,
|
||||
error: 'Failed to load users: ${e.toString()}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch tags
|
||||
Future<void> fetchTags() async {
|
||||
if (state.isLoadingTags) return;
|
||||
|
||||
state = state.copyWith(isLoadingTags: true, clearError: true);
|
||||
|
||||
try {
|
||||
final response = await _apiService.get(ApiConfig.tags);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
final data = response.data!;
|
||||
|
||||
// Parse tags from the response
|
||||
List<dynamic> tagsList = [];
|
||||
if (data['tags'] != null) {
|
||||
tagsList = data['tags'] as List<dynamic>? ?? [];
|
||||
} else if (data['results'] != null) {
|
||||
tagsList = data['results'] as List<dynamic>? ?? [];
|
||||
}
|
||||
|
||||
final tags = <TagLookup>[];
|
||||
for (final item in tagsList) {
|
||||
if (item is Map<String, dynamic>) {
|
||||
tags.add(TagLookup.fromJson(item));
|
||||
}
|
||||
}
|
||||
|
||||
state = state.copyWith(tags: tags, isLoadingTags: false);
|
||||
} else {
|
||||
state = state.copyWith(
|
||||
isLoadingTags: false,
|
||||
error: response.message ?? 'Failed to load tags',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
state = state.copyWith(
|
||||
isLoadingTags: false,
|
||||
error: 'Failed to load tags: ${e.toString()}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get account by ID
|
||||
AccountLookup? getAccountById(String? id) {
|
||||
if (id == null || id.isEmpty) return null;
|
||||
try {
|
||||
return state.accounts.firstWhere((a) => a.id == id);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get contact by ID
|
||||
ContactLookup? getContactById(String? id) {
|
||||
if (id == null || id.isEmpty) return null;
|
||||
try {
|
||||
return state.contacts.firstWhere((c) => c.id == id);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get user by ID
|
||||
UserLookup? getUserById(String? id) {
|
||||
if (id == null || id.isEmpty) return null;
|
||||
try {
|
||||
return state.users.firstWhere((u) => u.id == id);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get tag by ID
|
||||
TagLookup? getTagById(String? id) {
|
||||
if (id == null || id.isEmpty) return null;
|
||||
try {
|
||||
return state.tags.firstWhere((t) => t.id == id);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get contacts by IDs
|
||||
List<ContactLookup> getContactsByIds(List<String> ids) {
|
||||
return state.contacts.where((c) => ids.contains(c.id)).toList();
|
||||
}
|
||||
|
||||
/// Get users by IDs
|
||||
List<UserLookup> getUsersByIds(List<String> ids) {
|
||||
return state.users.where((u) => ids.contains(u.id)).toList();
|
||||
}
|
||||
|
||||
/// Get tags by IDs
|
||||
List<TagLookup> getTagsByIds(List<String> ids) {
|
||||
return state.tags.where((t) => ids.contains(t.id)).toList();
|
||||
}
|
||||
|
||||
/// Clear all data
|
||||
void clear() {
|
||||
state = const LookupState.initial();
|
||||
final profiles = response.data!['profiles'] as List<dynamic>? ?? [];
|
||||
return profiles
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(UserLookup.fromJson)
|
||||
.where((u) => u.isActive)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
/// Main lookup provider
|
||||
final lookupProvider = StateNotifierProvider<LookupNotifier, LookupState>((
|
||||
ref,
|
||||
) {
|
||||
return LookupNotifier();
|
||||
});
|
||||
final usersLookupProvider =
|
||||
AsyncNotifierProvider<UsersLookupNotifier, List<UserLookup>>(
|
||||
UsersLookupNotifier.new,
|
||||
);
|
||||
|
||||
/// Convenience providers
|
||||
/// Tags
|
||||
class TagsLookupNotifier extends AsyncNotifier<List<TagLookup>> {
|
||||
@override
|
||||
Future<List<TagLookup>> build() => _fetch();
|
||||
|
||||
Future<void> refresh() async {
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(_fetch);
|
||||
}
|
||||
|
||||
Future<List<TagLookup>> _fetch() async {
|
||||
final response = await _apiService.get(ApiConfig.tags);
|
||||
if (!response.success || response.data == null) {
|
||||
throw Exception(response.message ?? 'Failed to load tags');
|
||||
}
|
||||
final data = response.data!;
|
||||
List<dynamic> list = [];
|
||||
if (data['tags'] != null) {
|
||||
list = data['tags'] as List<dynamic>? ?? [];
|
||||
} else if (data['results'] != null) {
|
||||
list = data['results'] as List<dynamic>? ?? [];
|
||||
}
|
||||
return list
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(TagLookup.fromJson)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
final tagsLookupProvider =
|
||||
AsyncNotifierProvider<TagsLookupNotifier, List<TagLookup>>(
|
||||
TagsLookupNotifier.new,
|
||||
);
|
||||
|
||||
/// Convenience wrappers — return the resolved list (or empty during
|
||||
/// load/error). Forms that don't care about loading state watch these.
|
||||
final accountsProvider = Provider<List<AccountLookup>>((ref) {
|
||||
return ref.watch(lookupProvider).accounts;
|
||||
return ref.watch(accountsLookupProvider).value ?? const [];
|
||||
});
|
||||
|
||||
final contactsProvider = Provider<List<ContactLookup>>((ref) {
|
||||
return ref.watch(lookupProvider).contacts;
|
||||
return ref.watch(contactsLookupProvider).value ?? const [];
|
||||
});
|
||||
|
||||
final usersProvider = Provider<List<UserLookup>>((ref) {
|
||||
return ref.watch(lookupProvider).users;
|
||||
return ref.watch(usersLookupProvider).value ?? const [];
|
||||
});
|
||||
|
||||
final tagsProvider = Provider<List<TagLookup>>((ref) {
|
||||
return ref.watch(lookupProvider).tags;
|
||||
});
|
||||
|
||||
final lookupLoadingProvider = Provider<bool>((ref) {
|
||||
return ref.watch(lookupProvider).isLoading;
|
||||
return ref.watch(tagsLookupProvider).value ?? const [];
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,45 +7,28 @@ import '../services/api_service.dart';
|
|||
|
||||
export '../services/api_service.dart' show ApiResponse;
|
||||
|
||||
/// Tasks list state
|
||||
class TasksState {
|
||||
/// Paginated tasks snapshot — wrapped by AsyncValue.
|
||||
class TasksListData {
|
||||
final List<Task> tasks;
|
||||
final bool isLoading;
|
||||
final String? error;
|
||||
final int totalCount;
|
||||
final bool hasMore;
|
||||
final int currentOffset;
|
||||
|
||||
const TasksState({
|
||||
const TasksListData({
|
||||
this.tasks = const [],
|
||||
this.isLoading = false,
|
||||
this.error,
|
||||
this.totalCount = 0,
|
||||
this.hasMore = true,
|
||||
this.currentOffset = 0,
|
||||
});
|
||||
|
||||
const TasksState.initial()
|
||||
: tasks = const [],
|
||||
isLoading = false,
|
||||
error = null,
|
||||
totalCount = 0,
|
||||
hasMore = true,
|
||||
currentOffset = 0;
|
||||
|
||||
TasksState copyWith({
|
||||
TasksListData copyWith({
|
||||
List<Task>? tasks,
|
||||
bool? isLoading,
|
||||
String? error,
|
||||
int? totalCount,
|
||||
bool? hasMore,
|
||||
int? currentOffset,
|
||||
bool clearError = false,
|
||||
}) {
|
||||
return TasksState(
|
||||
return TasksListData(
|
||||
tasks: tasks ?? this.tasks,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
error: clearError ? null : (error ?? this.error),
|
||||
totalCount: totalCount ?? this.totalCount,
|
||||
hasMore: hasMore ?? this.hasMore,
|
||||
currentOffset: currentOffset ?? this.currentOffset,
|
||||
|
|
@ -53,293 +36,238 @@ class TasksState {
|
|||
}
|
||||
}
|
||||
|
||||
/// Tasks notifier for managing tasks state
|
||||
class TasksNotifier extends StateNotifier<TasksState> {
|
||||
TasksNotifier() : super(const TasksState.initial());
|
||||
|
||||
class TasksNotifier extends AsyncNotifier<TasksListData> {
|
||||
final ApiService _apiService = ApiService();
|
||||
static const int _pageSize = 20;
|
||||
|
||||
/// Fetch tasks from API
|
||||
Future<void> fetchTasks({
|
||||
@override
|
||||
Future<TasksListData> build() => _fetchPage(offset: 0);
|
||||
|
||||
Future<void> refresh({
|
||||
String? search,
|
||||
String? status,
|
||||
String? priority,
|
||||
bool refresh = false,
|
||||
}) async {
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(
|
||||
() => _fetchPage(
|
||||
offset: 0,
|
||||
search: search,
|
||||
status: status,
|
||||
priority: priority,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> loadMore({
|
||||
String? search,
|
||||
String? status,
|
||||
String? priority,
|
||||
}) async {
|
||||
final current = state.value;
|
||||
if (current == null || !current.hasMore) return;
|
||||
if (state.isLoading) return;
|
||||
|
||||
if (refresh) {
|
||||
state = state.copyWith(currentOffset: 0, hasMore: true, clearError: true);
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
'TasksNotifier: Fetching tasks (offset: ${state.currentOffset})...',
|
||||
);
|
||||
state = state.copyWith(isLoading: true, clearError: true);
|
||||
|
||||
try {
|
||||
// Build query parameters
|
||||
final queryParams = <String, String>{
|
||||
'limit': _pageSize.toString(),
|
||||
'offset': state.currentOffset.toString(),
|
||||
};
|
||||
|
||||
if (search != null && search.isNotEmpty) {
|
||||
queryParams['search'] = search;
|
||||
}
|
||||
if (status != null && status.isNotEmpty) {
|
||||
queryParams['status'] = status;
|
||||
}
|
||||
if (priority != null && priority.isNotEmpty) {
|
||||
queryParams['priority'] = priority;
|
||||
}
|
||||
|
||||
final url = Uri.parse(
|
||||
ApiConfig.tasks,
|
||||
).replace(queryParameters: queryParams).toString();
|
||||
final response = await _apiService.get(url);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
final data = response.data!;
|
||||
|
||||
debugPrint('TasksNotifier: API response keys: ${data.keys.toList()}');
|
||||
|
||||
// Parse tasks - handle different response formats
|
||||
List<dynamic> tasksList = [];
|
||||
int tasksCount = 0;
|
||||
|
||||
if (data['tasks'] != null) {
|
||||
tasksList = data['tasks'] as List<dynamic>? ?? [];
|
||||
tasksCount = data['tasks_count'] as int? ?? tasksList.length;
|
||||
} else if (data['results'] != null) {
|
||||
// Handle paginated response format
|
||||
tasksList = data['results'] as List<dynamic>? ?? [];
|
||||
tasksCount = data['count'] as int? ?? tasksList.length;
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
'TasksNotifier: Found ${tasksList.length} tasks in response',
|
||||
);
|
||||
|
||||
final newTasks = <Task>[];
|
||||
for (final item in tasksList) {
|
||||
try {
|
||||
if (item is Map<String, dynamic>) {
|
||||
newTasks.add(Task.fromJson(item));
|
||||
} else {
|
||||
debugPrint(
|
||||
'TasksNotifier: Skipping non-map item: ${item.runtimeType}',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('TasksNotifier: Error parsing task: $e');
|
||||
debugPrint('TasksNotifier: Task data: $item');
|
||||
}
|
||||
}
|
||||
|
||||
// Update state
|
||||
final updatedTasks = refresh ? newTasks : [...state.tasks, ...newTasks];
|
||||
|
||||
state = state.copyWith(
|
||||
tasks: updatedTasks,
|
||||
isLoading: false,
|
||||
totalCount: tasksCount,
|
||||
hasMore: newTasks.length >= _pageSize,
|
||||
currentOffset: state.currentOffset + newTasks.length,
|
||||
);
|
||||
|
||||
debugPrint(
|
||||
'TasksNotifier: Loaded ${newTasks.length} tasks (total: ${updatedTasks.length})',
|
||||
);
|
||||
} else {
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
error: response.message ?? 'Failed to load tasks',
|
||||
);
|
||||
debugPrint('TasksNotifier: API error - ${response.message}');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('TasksNotifier: Exception - $e');
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
error: 'Failed to load tasks: ${e.toString()}',
|
||||
state = await AsyncValue.guard(() async {
|
||||
final next = await _fetchPage(
|
||||
offset: current.currentOffset,
|
||||
search: search,
|
||||
status: status,
|
||||
priority: priority,
|
||||
);
|
||||
return current.copyWith(
|
||||
tasks: [...current.tasks, ...next.tasks],
|
||||
totalCount: next.totalCount,
|
||||
hasMore: next.hasMore,
|
||||
currentOffset: next.currentOffset,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<TasksListData> _fetchPage({
|
||||
required int offset,
|
||||
String? search,
|
||||
String? status,
|
||||
String? priority,
|
||||
}) async {
|
||||
final queryParams = <String, String>{
|
||||
'limit': _pageSize.toString(),
|
||||
'offset': offset.toString(),
|
||||
};
|
||||
if (search != null && search.isNotEmpty) queryParams['search'] = search;
|
||||
if (status != null && status.isNotEmpty) queryParams['status'] = status;
|
||||
if (priority != null && priority.isNotEmpty) {
|
||||
queryParams['priority'] = priority;
|
||||
}
|
||||
|
||||
final url = Uri.parse(
|
||||
ApiConfig.tasks,
|
||||
).replace(queryParameters: queryParams).toString();
|
||||
debugPrint('TasksNotifier: Fetching tasks (offset: $offset)...');
|
||||
final response = await _apiService.get(url);
|
||||
|
||||
if (!response.success || response.data == null) {
|
||||
throw Exception(response.message ?? 'Failed to load tasks');
|
||||
}
|
||||
|
||||
final data = response.data!;
|
||||
debugPrint('TasksNotifier: API response keys: ${data.keys.toList()}');
|
||||
|
||||
List<dynamic> tasksList = [];
|
||||
int tasksCount = 0;
|
||||
|
||||
if (data['tasks'] != null) {
|
||||
tasksList = data['tasks'] as List<dynamic>? ?? [];
|
||||
tasksCount = data['tasks_count'] as int? ?? tasksList.length;
|
||||
} else if (data['results'] != null) {
|
||||
tasksList = data['results'] as List<dynamic>? ?? [];
|
||||
tasksCount = data['count'] as int? ?? tasksList.length;
|
||||
}
|
||||
|
||||
final newTasks = <Task>[];
|
||||
for (final item in tasksList) {
|
||||
try {
|
||||
if (item is Map<String, dynamic>) {
|
||||
newTasks.add(Task.fromJson(item));
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('TasksNotifier: Error parsing task: $e');
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('TasksNotifier: loaded ${newTasks.length} tasks');
|
||||
|
||||
return TasksListData(
|
||||
tasks: newTasks,
|
||||
totalCount: tasksCount,
|
||||
hasMore: newTasks.length >= _pageSize,
|
||||
currentOffset: offset + newTasks.length,
|
||||
);
|
||||
}
|
||||
|
||||
/// Refresh tasks (reset and fetch first page)
|
||||
Future<void> refresh() async {
|
||||
state = const TasksState.initial();
|
||||
await fetchTasks(refresh: true);
|
||||
}
|
||||
|
||||
/// Load more tasks
|
||||
Future<void> loadMore() async {
|
||||
if (!state.hasMore || state.isLoading) return;
|
||||
await fetchTasks();
|
||||
}
|
||||
|
||||
/// Clear tasks data
|
||||
void clear() {
|
||||
state = const TasksState.initial();
|
||||
}
|
||||
|
||||
/// Get a single task by ID
|
||||
/// Fetch a single task from the API.
|
||||
Future<Task?> getTaskById(String taskId) async {
|
||||
try {
|
||||
final url = '${ApiConfig.tasks}$taskId/';
|
||||
debugPrint('TasksNotifier: Fetching task $taskId');
|
||||
|
||||
final response = await _apiService.get(url);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
// API returns { "task_obj": {...}, "attachments": [...], ... }
|
||||
final taskData = response.data!['task_obj'] as Map<String, dynamic>?;
|
||||
if (taskData != null) {
|
||||
debugPrint(
|
||||
'TasksNotifier: Task data keys: ${taskData.keys.toList()}',
|
||||
);
|
||||
return Task.fromJson(taskData);
|
||||
}
|
||||
debugPrint('TasksNotifier: task_obj not found in response');
|
||||
return null;
|
||||
} else {
|
||||
debugPrint('TasksNotifier: Failed to get task - ${response.message}');
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
} catch (e) {
|
||||
debugPrint('TasksNotifier: Exception getting task - $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new task
|
||||
Future<ApiResponse<Map<String, dynamic>>> createTask(
|
||||
Map<String, dynamic> data,
|
||||
) async {
|
||||
try {
|
||||
debugPrint('TasksNotifier: Creating task');
|
||||
final response = await _apiService.post(ApiConfig.tasks, data);
|
||||
|
||||
if (response.success) {
|
||||
// Refresh the list to include the new task
|
||||
await fetchTasks(refresh: true);
|
||||
}
|
||||
|
||||
if (response.success) await refresh();
|
||||
return response;
|
||||
} catch (e) {
|
||||
debugPrint('TasksNotifier: Exception creating task - $e');
|
||||
return ApiResponse(success: false, message: e.toString(), statusCode: 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Update an existing task
|
||||
Future<ApiResponse<Map<String, dynamic>>> updateTask(
|
||||
String taskId,
|
||||
Map<String, dynamic> data,
|
||||
) async {
|
||||
try {
|
||||
final url = '${ApiConfig.tasks}$taskId/';
|
||||
debugPrint('TasksNotifier: Updating task $taskId');
|
||||
|
||||
final response = await _apiService.put(url, data);
|
||||
|
||||
if (response.success) {
|
||||
debugPrint('TasksNotifier: Update successful, refreshing task list');
|
||||
// Refresh the list to get updated data from server
|
||||
await fetchTasks(refresh: true);
|
||||
}
|
||||
|
||||
if (response.success) await refresh();
|
||||
return response;
|
||||
} catch (e) {
|
||||
debugPrint('TasksNotifier: Exception updating task - $e');
|
||||
return ApiResponse(success: false, message: e.toString(), statusCode: 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a task
|
||||
Future<ApiResponse<Map<String, dynamic>>> deleteTask(String taskId) async {
|
||||
try {
|
||||
final url = '${ApiConfig.tasks}$taskId/';
|
||||
debugPrint('TasksNotifier: Deleting task $taskId');
|
||||
|
||||
final response = await _apiService.delete(url);
|
||||
|
||||
if (response.success || response.statusCode == 204) {
|
||||
// Remove from local state
|
||||
state = state.copyWith(
|
||||
tasks: state.tasks.where((t) => t.id != taskId).toList(),
|
||||
totalCount: state.totalCount - 1,
|
||||
);
|
||||
final current = state.value;
|
||||
if (current != null) {
|
||||
state = AsyncValue.data(
|
||||
current.copyWith(
|
||||
tasks: current.tasks.where((t) => t.id != taskId).toList(),
|
||||
totalCount: current.totalCount - 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
return ApiResponse(success: true, statusCode: 204);
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (e) {
|
||||
debugPrint('TasksNotifier: Exception deleting task - $e');
|
||||
return ApiResponse(success: false, message: e.toString(), statusCode: 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle task completion status
|
||||
/// Toggle a task's completion (helper for the UI checkbox).
|
||||
Future<ApiResponse<Map<String, dynamic>>> toggleTaskStatus(Task task) async {
|
||||
final newStatus = task.completed ? 'New' : 'Completed';
|
||||
return patchTask(task.id, {'status': newStatus});
|
||||
}
|
||||
|
||||
/// Partially update a task (PATCH)
|
||||
/// Partially update a task — optimistically applies changes locally.
|
||||
Future<ApiResponse<Map<String, dynamic>>> patchTask(
|
||||
String taskId,
|
||||
Map<String, dynamic> data,
|
||||
) async {
|
||||
try {
|
||||
final url = '${ApiConfig.tasks}$taskId/';
|
||||
debugPrint('TasksNotifier: Patching task $taskId with $data');
|
||||
|
||||
final response = await _apiService.patch(url, data);
|
||||
|
||||
if (response.success) {
|
||||
debugPrint(
|
||||
'TasksNotifier: Patch successful, updating local state optimistically',
|
||||
);
|
||||
// Update local state optimistically based on the patch data
|
||||
state = state.copyWith(
|
||||
tasks: state.tasks.map((t) {
|
||||
if (t.id == taskId) {
|
||||
// Apply patch data to existing task
|
||||
return t.copyWith(
|
||||
status: data.containsKey('status')
|
||||
? TaskStatus.fromString(data['status'] as String?)
|
||||
: null,
|
||||
priority: data.containsKey('priority')
|
||||
? Priority.fromString(data['priority'] as String?)
|
||||
: null,
|
||||
title: data['title'] as String?,
|
||||
description: data['description'] as String?,
|
||||
);
|
||||
}
|
||||
return t;
|
||||
}).toList(),
|
||||
);
|
||||
final current = state.value;
|
||||
if (current != null) {
|
||||
state = AsyncValue.data(
|
||||
current.copyWith(
|
||||
tasks: current.tasks.map((t) {
|
||||
if (t.id == taskId) {
|
||||
return t.copyWith(
|
||||
status: data.containsKey('status')
|
||||
? TaskStatus.fromString(data['status'] as String?)
|
||||
: null,
|
||||
priority: data.containsKey('priority')
|
||||
? Priority.fromString(data['priority'] as String?)
|
||||
: null,
|
||||
title: data['title'] as String?,
|
||||
description: data['description'] as String?,
|
||||
);
|
||||
}
|
||||
return t;
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (e) {
|
||||
debugPrint('TasksNotifier: Exception patching task - $e');
|
||||
return ApiResponse(success: false, message: e.toString(), statusCode: 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tasks provider
|
||||
final tasksProvider = StateNotifierProvider<TasksNotifier, TasksState>((ref) {
|
||||
return TasksNotifier();
|
||||
});
|
||||
final tasksProvider = AsyncNotifierProvider<TasksNotifier, TasksListData>(
|
||||
TasksNotifier.new,
|
||||
);
|
||||
|
||||
/// Convenience providers
|
||||
/// Convenience providers — read from the AsyncValue.
|
||||
final tasksListProvider = Provider<List<Task>>((ref) {
|
||||
return ref.watch(tasksProvider).tasks;
|
||||
return ref.watch(tasksProvider).value?.tasks ?? const [];
|
||||
});
|
||||
|
||||
final tasksLoadingProvider = Provider<bool>((ref) {
|
||||
|
|
@ -347,10 +275,10 @@ final tasksLoadingProvider = Provider<bool>((ref) {
|
|||
});
|
||||
|
||||
final tasksErrorProvider = Provider<String?>((ref) {
|
||||
return ref.watch(tasksProvider).error;
|
||||
return ref.watch(tasksProvider).error?.toString();
|
||||
});
|
||||
|
||||
/// Grouped tasks providers
|
||||
/// Grouped tasks providers (filters / sorters on top of the loaded list).
|
||||
final overdueTasksProvider = Provider<List<Task>>((ref) {
|
||||
final tasks = ref.watch(tasksListProvider);
|
||||
return tasks.where((t) => t.isOverdue).toList()..sort((a, b) {
|
||||
|
|
|
|||
|
|
@ -4,45 +4,28 @@ import '../data/models/ticket.dart';
|
|||
import '../data/models/comment.dart';
|
||||
import '../services/api_service.dart';
|
||||
|
||||
/// Tickets list state
|
||||
class TicketsState {
|
||||
/// Paginated tickets snapshot — wrapped by AsyncValue.
|
||||
class TicketsListData {
|
||||
final List<Ticket> tickets;
|
||||
final bool isLoading;
|
||||
final String? error;
|
||||
final int totalCount;
|
||||
final bool hasMore;
|
||||
final int currentOffset;
|
||||
|
||||
const TicketsState({
|
||||
const TicketsListData({
|
||||
this.tickets = const [],
|
||||
this.isLoading = false,
|
||||
this.error,
|
||||
this.totalCount = 0,
|
||||
this.hasMore = true,
|
||||
this.currentOffset = 0,
|
||||
});
|
||||
|
||||
const TicketsState.initial()
|
||||
: tickets = const [],
|
||||
isLoading = false,
|
||||
error = null,
|
||||
totalCount = 0,
|
||||
hasMore = true,
|
||||
currentOffset = 0;
|
||||
|
||||
TicketsState copyWith({
|
||||
TicketsListData copyWith({
|
||||
List<Ticket>? tickets,
|
||||
bool? isLoading,
|
||||
String? error,
|
||||
int? totalCount,
|
||||
bool? hasMore,
|
||||
int? currentOffset,
|
||||
bool clearError = false,
|
||||
}) {
|
||||
return TicketsState(
|
||||
return TicketsListData(
|
||||
tickets: tickets ?? this.tickets,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
error: clearError ? null : (error ?? this.error),
|
||||
totalCount: totalCount ?? this.totalCount,
|
||||
hasMore: hasMore ?? this.hasMore,
|
||||
currentOffset: currentOffset ?? this.currentOffset,
|
||||
|
|
@ -50,21 +33,109 @@ class TicketsState {
|
|||
}
|
||||
}
|
||||
|
||||
/// Tickets notifier mirroring the leads notifier
|
||||
class TicketsNotifier extends StateNotifier<TicketsState> {
|
||||
TicketsNotifier() : super(const TicketsState.initial());
|
||||
|
||||
class TicketsNotifier extends AsyncNotifier<TicketsListData> {
|
||||
final ApiService _apiService = ApiService();
|
||||
static const int _pageSize = 20;
|
||||
|
||||
/// Get a single ticket by ID, returning the full detail (incl. comments).
|
||||
@override
|
||||
Future<TicketsListData> build() => _fetchPage(offset: 0);
|
||||
|
||||
Future<void> refresh({
|
||||
String? search,
|
||||
String? status,
|
||||
String? priority,
|
||||
String? assignedTo,
|
||||
}) async {
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(
|
||||
() => _fetchPage(
|
||||
offset: 0,
|
||||
search: search,
|
||||
status: status,
|
||||
priority: priority,
|
||||
assignedTo: assignedTo,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> loadMore({
|
||||
String? search,
|
||||
String? status,
|
||||
String? priority,
|
||||
String? assignedTo,
|
||||
}) async {
|
||||
final current = state.value;
|
||||
if (current == null || !current.hasMore) return;
|
||||
if (state.isLoading) return;
|
||||
|
||||
state = await AsyncValue.guard(() async {
|
||||
final next = await _fetchPage(
|
||||
offset: current.currentOffset,
|
||||
search: search,
|
||||
status: status,
|
||||
priority: priority,
|
||||
assignedTo: assignedTo,
|
||||
);
|
||||
return current.copyWith(
|
||||
tickets: [...current.tickets, ...next.tickets],
|
||||
totalCount: next.totalCount,
|
||||
hasMore: next.hasMore,
|
||||
currentOffset: next.currentOffset,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<TicketsListData> _fetchPage({
|
||||
required int offset,
|
||||
String? search,
|
||||
String? status,
|
||||
String? priority,
|
||||
String? assignedTo,
|
||||
}) async {
|
||||
final queryParams = <String, String>{
|
||||
'limit': _pageSize.toString(),
|
||||
'offset': offset.toString(),
|
||||
};
|
||||
if (search != null && search.isNotEmpty) queryParams['search'] = search;
|
||||
if (status != null && status.isNotEmpty) queryParams['status'] = status;
|
||||
if (priority != null && priority.isNotEmpty) {
|
||||
queryParams['priority'] = priority;
|
||||
}
|
||||
if (assignedTo != null && assignedTo.isNotEmpty) {
|
||||
queryParams['assigned_to'] = assignedTo;
|
||||
}
|
||||
|
||||
final url = Uri.parse(
|
||||
ApiConfig.tickets,
|
||||
).replace(queryParameters: queryParams).toString();
|
||||
final response = await _apiService.get(url);
|
||||
|
||||
if (!response.success || response.data == null) {
|
||||
throw Exception(response.message ?? 'Failed to load tickets');
|
||||
}
|
||||
|
||||
final data = response.data!;
|
||||
final ticketsList = data['cases'] as List<dynamic>? ?? [];
|
||||
final newTickets = ticketsList
|
||||
.map((j) => Ticket.fromJson(j as Map<String, dynamic>))
|
||||
.toList();
|
||||
final totalCount = data['cases_count'] as int? ?? newTickets.length;
|
||||
|
||||
return TicketsListData(
|
||||
tickets: newTickets,
|
||||
totalCount: totalCount,
|
||||
hasMore: newTickets.length >= _pageSize,
|
||||
currentOffset: offset + newTickets.length,
|
||||
);
|
||||
}
|
||||
|
||||
/// Fetch a single ticket (just the Ticket object).
|
||||
Future<Ticket?> getTicketById(String id) async {
|
||||
final detail = await getTicketDetail(id);
|
||||
return detail?.ticketObj;
|
||||
}
|
||||
|
||||
/// Get the full ticket detail including activities + comment permission.
|
||||
/// Returns null if the ticket can't be loaded.
|
||||
/// Fetch the full ticket detail (incl. comments + activities + permissions).
|
||||
Future<TicketDetailResult?> getTicketDetail(String id) async {
|
||||
try {
|
||||
final response = await _apiService.get(ApiConfig.ticketDetail(id));
|
||||
|
|
@ -75,7 +146,7 @@ class TicketsNotifier extends StateNotifier<TicketsState> {
|
|||
var ticketObj = Ticket.fromJson(ticketData);
|
||||
|
||||
// Public + internal comments are sent in two separate arrays. We tag
|
||||
// each one so the UI can render an "Internal" badge without an extra
|
||||
// each so the UI can render an "Internal" badge without an extra
|
||||
// round-trip — `Comment` itself doesn't carry that flag yet.
|
||||
final List<_TaggedComment> tagged = [];
|
||||
for (final c in (response.data!['comments'] as List<dynamic>? ?? [])) {
|
||||
|
|
@ -123,22 +194,18 @@ class TicketsNotifier extends StateNotifier<TicketsState> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Create a new ticket
|
||||
Future<ApiResponse<Map<String, dynamic>>> createTicket(
|
||||
Map<String, dynamic> ticketData,
|
||||
) async {
|
||||
try {
|
||||
final response = await _apiService.post(ApiConfig.tickets, ticketData);
|
||||
if (response.success) {
|
||||
await refresh();
|
||||
}
|
||||
if (response.success) await refresh();
|
||||
return response;
|
||||
} catch (e) {
|
||||
return ApiResponse(success: false, message: e.toString(), statusCode: 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Update an existing ticket
|
||||
Future<ApiResponse<Map<String, dynamic>>> updateTicket(
|
||||
String id,
|
||||
Map<String, dynamic> ticketData,
|
||||
|
|
@ -148,16 +215,13 @@ class TicketsNotifier extends StateNotifier<TicketsState> {
|
|||
ApiConfig.ticketDetail(id),
|
||||
ticketData,
|
||||
);
|
||||
if (response.success) {
|
||||
await refresh();
|
||||
}
|
||||
if (response.success) await refresh();
|
||||
return response;
|
||||
} catch (e) {
|
||||
return ApiResponse(success: false, message: e.toString(), statusCode: 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a comment (or internal note) to a ticket.
|
||||
Future<ApiResponse<Map<String, dynamic>>> addComment(
|
||||
String ticketId,
|
||||
String comment, {
|
||||
|
|
@ -174,113 +238,35 @@ class TicketsNotifier extends StateNotifier<TicketsState> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Delete a ticket
|
||||
Future<ApiResponse<Map<String, dynamic>>> deleteTicket(String id) async {
|
||||
try {
|
||||
final response = await _apiService.delete(ApiConfig.ticketDetail(id));
|
||||
if (response.success) {
|
||||
state = state.copyWith(
|
||||
tickets: state.tickets.where((c) => c.id != id).toList(),
|
||||
totalCount: state.totalCount > 0 ? state.totalCount - 1 : 0,
|
||||
);
|
||||
final current = state.value;
|
||||
if (current != null) {
|
||||
state = AsyncValue.data(
|
||||
current.copyWith(
|
||||
tickets: current.tickets.where((t) => t.id != id).toList(),
|
||||
totalCount: current.totalCount > 0 ? current.totalCount - 1 : 0,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
return response;
|
||||
} catch (e) {
|
||||
return ApiResponse(success: false, message: e.toString(), statusCode: 0);
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch the tickets list with optional filters.
|
||||
Future<void> fetchTickets({
|
||||
String? search,
|
||||
String? status,
|
||||
String? priority,
|
||||
String? assignedTo,
|
||||
bool refresh = false,
|
||||
}) async {
|
||||
if (state.isLoading) return;
|
||||
|
||||
if (refresh) {
|
||||
state = state.copyWith(currentOffset: 0, hasMore: true, clearError: true);
|
||||
}
|
||||
|
||||
state = state.copyWith(isLoading: true, clearError: true);
|
||||
|
||||
try {
|
||||
final queryParams = <String, String>{
|
||||
'limit': _pageSize.toString(),
|
||||
'offset': state.currentOffset.toString(),
|
||||
};
|
||||
if (search != null && search.isNotEmpty) queryParams['search'] = search;
|
||||
if (status != null && status.isNotEmpty) queryParams['status'] = status;
|
||||
if (priority != null && priority.isNotEmpty) {
|
||||
queryParams['priority'] = priority;
|
||||
}
|
||||
if (assignedTo != null && assignedTo.isNotEmpty) {
|
||||
queryParams['assigned_to'] = assignedTo;
|
||||
}
|
||||
|
||||
final url = Uri.parse(
|
||||
ApiConfig.tickets,
|
||||
).replace(queryParameters: queryParams).toString();
|
||||
final response = await _apiService.get(url);
|
||||
|
||||
if (response.success && response.data != null) {
|
||||
final data = response.data!;
|
||||
final ticketsList = data['cases'] as List<dynamic>? ?? [];
|
||||
final newTickets = ticketsList
|
||||
.map((j) => Ticket.fromJson(j as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
final updatedTickets = refresh
|
||||
? newTickets
|
||||
: [...state.tickets, ...newTickets];
|
||||
final totalCount = data['cases_count'] as int? ?? updatedTickets.length;
|
||||
|
||||
state = state.copyWith(
|
||||
tickets: updatedTickets,
|
||||
isLoading: false,
|
||||
totalCount: totalCount,
|
||||
hasMore: newTickets.length >= _pageSize,
|
||||
currentOffset: state.currentOffset + newTickets.length,
|
||||
);
|
||||
} else {
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
error: response.message ?? 'Failed to load tickets',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
error: 'Failed to load tickets: ${e.toString()}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> refresh() async {
|
||||
state = const TicketsState.initial();
|
||||
await fetchTickets(refresh: true);
|
||||
}
|
||||
|
||||
Future<void> loadMore() async {
|
||||
if (!state.hasMore || state.isLoading) return;
|
||||
await fetchTickets();
|
||||
}
|
||||
|
||||
void clear() {
|
||||
state = const TicketsState.initial();
|
||||
}
|
||||
}
|
||||
|
||||
final ticketsProvider = StateNotifierProvider<TicketsNotifier, TicketsState>((
|
||||
ref,
|
||||
) {
|
||||
return TicketsNotifier();
|
||||
});
|
||||
final ticketsProvider =
|
||||
AsyncNotifierProvider<TicketsNotifier, TicketsListData>(
|
||||
TicketsNotifier.new,
|
||||
);
|
||||
|
||||
/// Convenience providers — read from the AsyncValue.
|
||||
final ticketsListProvider = Provider<List<Ticket>>((ref) {
|
||||
return ref.watch(ticketsProvider).tickets;
|
||||
return ref.watch(ticketsProvider).value?.tickets ?? const [];
|
||||
});
|
||||
|
||||
final ticketsLoadingProvider = Provider<bool>((ref) {
|
||||
|
|
@ -288,7 +274,7 @@ final ticketsLoadingProvider = Provider<bool>((ref) {
|
|||
});
|
||||
|
||||
final ticketsErrorProvider = Provider<String?>((ref) {
|
||||
return ref.watch(ticketsProvider).error;
|
||||
return ref.watch(ticketsProvider).error?.toString();
|
||||
});
|
||||
|
||||
/// Bundled detail-fetch result. The mobile detail screen needs more than
|
||||
|
|
|
|||
|
|
@ -19,18 +19,9 @@ class DashboardScreen extends ConsumerStatefulWidget {
|
|||
}
|
||||
|
||||
class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Fetch dashboard data when screen loads
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ref.read(dashboardProvider.notifier).fetchDashboard();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final dashboardState = ref.watch(dashboardProvider);
|
||||
final dashboardAsync = ref.watch(dashboardProvider);
|
||||
final authState = ref.watch(authProvider);
|
||||
final userName = authState.user?.displayName ?? 'User';
|
||||
final currencySymbol =
|
||||
|
|
@ -81,7 +72,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||
// Content
|
||||
SliverToBoxAdapter(
|
||||
child: _buildContent(
|
||||
dashboardState,
|
||||
dashboardAsync,
|
||||
currencyFormat,
|
||||
compactCurrencyFormat,
|
||||
),
|
||||
|
|
@ -94,18 +85,21 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||
}
|
||||
|
||||
Widget _buildContent(
|
||||
DashboardState dashboardState,
|
||||
AsyncValue<DashboardData> dashboardAsync,
|
||||
NumberFormat currencyFormat,
|
||||
NumberFormat compactCurrencyFormat,
|
||||
) {
|
||||
if (dashboardState.isLoading && dashboardState.data == null) {
|
||||
// Stale-while-loading: keep showing prior data during refresh.
|
||||
final data = dashboardAsync.value;
|
||||
|
||||
if (data == null && dashboardAsync.isLoading) {
|
||||
return const SizedBox(
|
||||
height: 400,
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
if (dashboardState.error != null && dashboardState.data == null) {
|
||||
if (data == null && dashboardAsync.hasError) {
|
||||
return SizedBox(
|
||||
height: 400,
|
||||
child: Center(
|
||||
|
|
@ -129,8 +123,15 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
|||
);
|
||||
}
|
||||
|
||||
final data = dashboardState.data ?? const DashboardData();
|
||||
final resolved = data ?? const DashboardData();
|
||||
return _buildBody(resolved, currencyFormat, compactCurrencyFormat);
|
||||
}
|
||||
|
||||
Widget _buildBody(
|
||||
DashboardData data,
|
||||
NumberFormat currencyFormat,
|
||||
NumberFormat compactCurrencyFormat,
|
||||
) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
|
|
|
|||
|
|
@ -49,11 +49,6 @@ class _DealFormScreenState extends ConsumerState<DealFormScreen> {
|
|||
super.initState();
|
||||
_probabilityController.text = _stage.defaultProbability.toString();
|
||||
|
||||
// Fetch lookup data
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ref.read(lookupProvider.notifier).fetchAll();
|
||||
});
|
||||
|
||||
if (widget.initialDeal != null) {
|
||||
_populateFromDeal(widget.initialDeal!);
|
||||
} else if (widget.isEditMode) {
|
||||
|
|
@ -185,8 +180,8 @@ class _DealFormScreenState extends ConsumerState<DealFormScreen> {
|
|||
_stage.defaultProbability;
|
||||
|
||||
// Get account name from lookup
|
||||
final lookupState = ref.read(lookupProvider);
|
||||
final account = lookupState.accounts
|
||||
final accounts = ref.read(accountsProvider);
|
||||
final account = accounts
|
||||
.where((a) => a.id == _selectedAccountId)
|
||||
.firstOrNull;
|
||||
|
||||
|
|
@ -580,7 +575,15 @@ class _DealFormScreenState extends ConsumerState<DealFormScreen> {
|
|||
}
|
||||
|
||||
Widget _buildRelationshipFields() {
|
||||
final lookupState = ref.watch(lookupProvider);
|
||||
final accountsAsync = ref.watch(accountsLookupProvider);
|
||||
final contactsAsync = ref.watch(contactsLookupProvider);
|
||||
final usersAsync = ref.watch(usersLookupProvider);
|
||||
final tagsAsync = ref.watch(tagsLookupProvider);
|
||||
|
||||
final accounts = accountsAsync.value ?? const [];
|
||||
final contacts = contactsAsync.value ?? const [];
|
||||
final users = usersAsync.value ?? const [];
|
||||
final tags = tagsAsync.value ?? const [];
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
|
|
@ -589,14 +592,14 @@ class _DealFormScreenState extends ConsumerState<DealFormScreen> {
|
|||
_buildSingleSelectField(
|
||||
label: 'Account',
|
||||
value: _selectedAccountId != null
|
||||
? lookupState.accounts
|
||||
? accounts
|
||||
.where((a) => a.id == _selectedAccountId)
|
||||
.firstOrNull
|
||||
?.name
|
||||
: null,
|
||||
placeholder: 'Select account',
|
||||
icon: LucideIcons.building2,
|
||||
isLoading: lookupState.isLoadingAccounts,
|
||||
isLoading: accountsAsync.isLoading,
|
||||
onTap: _showAccountPicker,
|
||||
onClear: _selectedAccountId != null
|
||||
? () => setState(() => _selectedAccountId = null)
|
||||
|
|
@ -612,7 +615,7 @@ class _DealFormScreenState extends ConsumerState<DealFormScreen> {
|
|||
selectedItems: _selectedContactIds
|
||||
.map(
|
||||
(id) =>
|
||||
lookupState.contacts
|
||||
contacts
|
||||
.where((c) => c.id == id)
|
||||
.firstOrNull
|
||||
?.fullName ??
|
||||
|
|
@ -622,7 +625,7 @@ class _DealFormScreenState extends ConsumerState<DealFormScreen> {
|
|||
.toList(),
|
||||
placeholder: 'Select contacts',
|
||||
icon: LucideIcons.users,
|
||||
isLoading: lookupState.isLoadingContacts,
|
||||
isLoading: contactsAsync.isLoading,
|
||||
onTap: _showContactsPicker,
|
||||
),
|
||||
|
||||
|
|
@ -635,7 +638,7 @@ class _DealFormScreenState extends ConsumerState<DealFormScreen> {
|
|||
selectedItems: _selectedAssignedToIds
|
||||
.map(
|
||||
(id) =>
|
||||
lookupState.users
|
||||
users
|
||||
.where((u) => u.id == id)
|
||||
.firstOrNull
|
||||
?.displayName ??
|
||||
|
|
@ -645,7 +648,7 @@ class _DealFormScreenState extends ConsumerState<DealFormScreen> {
|
|||
.toList(),
|
||||
placeholder: 'Select assignees',
|
||||
icon: LucideIcons.userCheck,
|
||||
isLoading: lookupState.isLoadingUsers,
|
||||
isLoading: usersAsync.isLoading,
|
||||
onTap: _showAssignedToPicker,
|
||||
),
|
||||
|
||||
|
|
@ -658,7 +661,7 @@ class _DealFormScreenState extends ConsumerState<DealFormScreen> {
|
|||
selectedItems: _selectedTagIds
|
||||
.map(
|
||||
(id) =>
|
||||
lookupState.tags
|
||||
tags
|
||||
.where((t) => t.id == id)
|
||||
.firstOrNull
|
||||
?.name ??
|
||||
|
|
@ -668,7 +671,7 @@ class _DealFormScreenState extends ConsumerState<DealFormScreen> {
|
|||
.toList(),
|
||||
placeholder: 'Select tags',
|
||||
icon: LucideIcons.tag,
|
||||
isLoading: lookupState.isLoadingTags,
|
||||
isLoading: tagsAsync.isLoading,
|
||||
onTap: _showTagsPicker,
|
||||
),
|
||||
],
|
||||
|
|
@ -1132,7 +1135,7 @@ class _DealFormScreenState extends ConsumerState<DealFormScreen> {
|
|||
}
|
||||
|
||||
void _showAccountPicker() {
|
||||
final accounts = ref.read(lookupProvider).accounts;
|
||||
final accounts = ref.read(accountsProvider);
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: AppColors.surface,
|
||||
|
|
@ -1168,7 +1171,7 @@ class _DealFormScreenState extends ConsumerState<DealFormScreen> {
|
|||
}
|
||||
|
||||
void _showContactsPicker() {
|
||||
final contacts = ref.read(lookupProvider).contacts;
|
||||
final contacts = ref.read(contactsProvider);
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: AppColors.surface,
|
||||
|
|
@ -1207,7 +1210,7 @@ class _DealFormScreenState extends ConsumerState<DealFormScreen> {
|
|||
}
|
||||
|
||||
void _showAssignedToPicker() {
|
||||
final users = ref.read(lookupProvider).users;
|
||||
final users = ref.read(usersProvider);
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: AppColors.surface,
|
||||
|
|
@ -1245,7 +1248,7 @@ class _DealFormScreenState extends ConsumerState<DealFormScreen> {
|
|||
}
|
||||
|
||||
void _showTagsPicker() {
|
||||
final tags = ref.read(lookupProvider).tags;
|
||||
final tags = ref.read(tagsProvider);
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: AppColors.surface,
|
||||
|
|
|
|||
|
|
@ -38,15 +38,6 @@ class _DealsListScreenState extends ConsumerState<DealsListScreen> {
|
|||
DealStage.closedWon,
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Fetch deals on screen load
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ref.read(dealsProvider.notifier).fetchDeals(refresh: true);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
|
|
|
|||
|
|
@ -616,7 +616,7 @@ class _LeadDetailScreenState extends ConsumerState<LeadDetailScreen>
|
|||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
if (action != null) action,
|
||||
?action,
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
|
|
|||
|
|
@ -53,10 +53,6 @@ class _LeadFormScreenState extends ConsumerState<LeadFormScreen> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Fetch lookup data
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ref.read(lookupProvider.notifier).fetchAll();
|
||||
});
|
||||
|
||||
if (widget.initialLead != null) {
|
||||
_populateFromLead(widget.initialLead!);
|
||||
|
|
@ -617,7 +613,8 @@ class _LeadFormScreenState extends ConsumerState<LeadFormScreen> {
|
|||
}
|
||||
|
||||
Widget _buildRelationshipFields() {
|
||||
final lookupState = ref.watch(lookupProvider);
|
||||
final users = ref.watch(usersProvider);
|
||||
final tags = ref.watch(tagsProvider);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
|
|
@ -626,7 +623,7 @@ class _LeadFormScreenState extends ConsumerState<LeadFormScreen> {
|
|||
label: 'Assigned To',
|
||||
icon: LucideIcons.users,
|
||||
selectedCount: _assignedToIds.length,
|
||||
selectedLabel: _getSelectedUsersLabel(lookupState.users),
|
||||
selectedLabel: _getSelectedUsersLabel(users),
|
||||
onTap: () => _showAssignedToPicker(),
|
||||
),
|
||||
|
||||
|
|
@ -637,7 +634,7 @@ class _LeadFormScreenState extends ConsumerState<LeadFormScreen> {
|
|||
label: 'Tags',
|
||||
icon: LucideIcons.tag,
|
||||
selectedCount: _tagIds.length,
|
||||
selectedLabel: _getSelectedTagsLabel(lookupState.tags),
|
||||
selectedLabel: _getSelectedTagsLabel(tags),
|
||||
onTap: () => _showTagsPicker(),
|
||||
),
|
||||
],
|
||||
|
|
@ -868,7 +865,7 @@ class _LeadFormScreenState extends ConsumerState<LeadFormScreen> {
|
|||
}
|
||||
|
||||
void _showAssignedToPicker() {
|
||||
final users = ref.read(lookupProvider).users;
|
||||
final users = ref.read(usersProvider);
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
|
|
@ -892,7 +889,7 @@ class _LeadFormScreenState extends ConsumerState<LeadFormScreen> {
|
|||
}
|
||||
|
||||
void _showTagsPicker() {
|
||||
final tags = ref.read(lookupProvider).tags;
|
||||
final tags = ref.read(tagsProvider);
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
|
|
|
|||
|
|
@ -30,12 +30,6 @@ class _LeadsListScreenState extends ConsumerState<LeadsListScreen> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Fetch leads when screen loads
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ref.read(leadsProvider.notifier).fetchLeads(refresh: true);
|
||||
});
|
||||
|
||||
// Setup scroll listener for pagination
|
||||
_scrollController.addListener(_onScroll);
|
||||
}
|
||||
|
||||
|
|
@ -124,8 +118,9 @@ class _LeadsListScreenState extends ConsumerState<LeadsListScreen> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final leadsState = ref.watch(leadsProvider);
|
||||
final filteredLeads = _filterAndSortLeads(leadsState.leads);
|
||||
final leadsAsync = ref.watch(leadsProvider);
|
||||
final data = leadsAsync.value;
|
||||
final filteredLeads = _filterAndSortLeads(data?.leads ?? const []);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.surfaceDim,
|
||||
|
|
@ -150,23 +145,28 @@ class _LeadsListScreenState extends ConsumerState<LeadsListScreen> {
|
|||
_buildFilterBar(),
|
||||
|
||||
// Results Count
|
||||
_buildResultsCount(filteredLeads.length, leadsState.totalCount),
|
||||
_buildResultsCount(filteredLeads.length, data?.totalCount ?? 0),
|
||||
|
||||
// Leads List
|
||||
Expanded(child: _buildLeadsList(leadsState, filteredLeads)),
|
||||
Expanded(child: _buildLeadsList(leadsAsync, filteredLeads)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLeadsList(LeadsState state, List<Lead> filteredLeads) {
|
||||
Widget _buildLeadsList(
|
||||
AsyncValue<LeadsListData> async,
|
||||
List<Lead> filteredLeads,
|
||||
) {
|
||||
final data = async.value;
|
||||
|
||||
// Initial loading
|
||||
if (state.isLoading && state.leads.isEmpty) {
|
||||
if (async.isLoading && (data == null || data.leads.isEmpty)) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
// Error state
|
||||
if (state.error != null && state.leads.isEmpty) {
|
||||
if (async.hasError && (data == null || data.leads.isEmpty)) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
|
@ -179,7 +179,7 @@ class _LeadsListScreenState extends ConsumerState<LeadsListScreen> {
|
|||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
state.error!,
|
||||
async.error.toString(),
|
||||
style: AppTypography.caption,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
|
@ -198,6 +198,8 @@ class _LeadsListScreenState extends ConsumerState<LeadsListScreen> {
|
|||
return _buildEmptyState();
|
||||
}
|
||||
|
||||
final hasMore = data?.hasMore ?? false;
|
||||
|
||||
// Leads list
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => ref.read(leadsProvider.notifier).refresh(),
|
||||
|
|
@ -205,11 +207,9 @@ class _LeadsListScreenState extends ConsumerState<LeadsListScreen> {
|
|||
controller: _scrollController,
|
||||
padding: const EdgeInsets.fromLTRB(12, 0, 12, 80),
|
||||
itemCount:
|
||||
filteredLeads.length +
|
||||
(state.hasMore && !_hasActiveFilters ? 1 : 0),
|
||||
filteredLeads.length + (hasMore && !_hasActiveFilters ? 1 : 0),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == filteredLeads.length) {
|
||||
// Loading indicator at bottom
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(12),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
|
|
|
|||
|
|
@ -26,14 +26,6 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen> {
|
|||
DateTime _selectedDay = DateTime.now();
|
||||
CalendarFormat _calendarFormat = CalendarFormat.month;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// Fetch tasks on init
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ref.read(tasksProvider.notifier).fetchTasks(refresh: true);
|
||||
});
|
||||
}
|
||||
|
||||
// Get task dates for calendar markers
|
||||
Set<String> _getTaskDates(List<Task> tasks) {
|
||||
|
|
@ -57,10 +49,10 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tasksState = ref.watch(tasksProvider);
|
||||
final allTasks = tasksState.tasks;
|
||||
final isLoading = tasksState.isLoading;
|
||||
final error = tasksState.error;
|
||||
final tasksAsync = ref.watch(tasksProvider);
|
||||
final allTasks = tasksAsync.value?.tasks ?? const <Task>[];
|
||||
final isLoading = tasksAsync.isLoading;
|
||||
final error = tasksAsync.error?.toString();
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.surfaceDim,
|
||||
|
|
@ -132,7 +124,7 @@ class _TasksListScreenState extends ConsumerState<TasksListScreen> {
|
|||
label: 'Retry',
|
||||
icon: LucideIcons.refreshCw,
|
||||
onPressed: () {
|
||||
ref.read(tasksProvider.notifier).fetchTasks(refresh: true);
|
||||
ref.read(tasksProvider.notifier).refresh();
|
||||
},
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -38,9 +38,6 @@ class _TicketFormScreenState extends ConsumerState<TicketFormScreen> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ref.read(lookupProvider.notifier).fetchAccounts();
|
||||
});
|
||||
if (widget.isEditMode) _fetchTicket();
|
||||
}
|
||||
|
||||
|
|
@ -174,7 +171,7 @@ class _TicketFormScreenState extends ConsumerState<TicketFormScreen> {
|
|||
),
|
||||
);
|
||||
}
|
||||
final accounts = ref.watch(lookupProvider).accounts;
|
||||
final accounts = ref.watch(accountsProvider);
|
||||
|
||||
return Form(
|
||||
key: _formKey,
|
||||
|
|
|
|||
|
|
@ -28,9 +28,6 @@ class _TicketsListScreenState extends ConsumerState<TicketsListScreen> {
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ref.read(ticketsProvider.notifier).fetchTickets(refresh: true);
|
||||
});
|
||||
_scrollController.addListener(_onScroll);
|
||||
}
|
||||
|
||||
|
|
@ -80,8 +77,9 @@ class _TicketsListScreenState extends ConsumerState<TicketsListScreen> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = ref.watch(ticketsProvider);
|
||||
final filtered = _filterTickets(state.tickets);
|
||||
final ticketsAsync = ref.watch(ticketsProvider);
|
||||
final data = ticketsAsync.value;
|
||||
final filtered = _filterTickets(data?.tickets ?? const []);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.surfaceDim,
|
||||
|
|
@ -101,18 +99,23 @@ class _TicketsListScreenState extends ConsumerState<TicketsListScreen> {
|
|||
children: [
|
||||
_buildSearchBar(),
|
||||
_buildFilterBar(),
|
||||
_buildResultsCount(filtered.length, state.totalCount),
|
||||
Expanded(child: _buildList(state, filtered)),
|
||||
_buildResultsCount(filtered.length, data?.totalCount ?? 0),
|
||||
Expanded(child: _buildList(ticketsAsync, filtered)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildList(TicketsState state, List<Ticket> filtered) {
|
||||
if (state.isLoading && state.tickets.isEmpty) {
|
||||
Widget _buildList(
|
||||
AsyncValue<TicketsListData> async,
|
||||
List<Ticket> filtered,
|
||||
) {
|
||||
final data = async.value;
|
||||
|
||||
if (async.isLoading && (data == null || data.tickets.isEmpty)) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (state.error != null && state.tickets.isEmpty) {
|
||||
if (async.hasError && (data == null || data.tickets.isEmpty)) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
|
@ -125,7 +128,7 @@ class _TicketsListScreenState extends ConsumerState<TicketsListScreen> {
|
|||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
state.error!,
|
||||
async.error.toString(),
|
||||
style: AppTypography.caption,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
|
@ -140,13 +143,15 @@ class _TicketsListScreenState extends ConsumerState<TicketsListScreen> {
|
|||
}
|
||||
if (filtered.isEmpty) return _buildEmptyState();
|
||||
|
||||
final hasMore = data?.hasMore ?? false;
|
||||
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => ref.read(ticketsProvider.notifier).refresh(),
|
||||
child: ListView.builder(
|
||||
controller: _scrollController,
|
||||
padding: const EdgeInsets.fromLTRB(12, 0, 12, 80),
|
||||
itemCount:
|
||||
filtered.length + (state.hasMore && !_hasActiveFilters ? 1 : 0),
|
||||
filtered.length + (hasMore && !_hasActiveFilters ? 1 : 0),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == filtered.length) {
|
||||
return const Padding(
|
||||
|
|
|
|||
|
|
@ -1,14 +1,30 @@
|
|||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
_fe_analyzer_shared:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: "8d7ff3948166b8ec5da0fbb5962000926b8e02f2ed9b3e51d1738905fbd4c98d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "93.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: de7148ed2fcec579b19f122c1800933dfa028f6d9fd38a152b04b1516cec120b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "10.0.1"
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: archive
|
||||
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
|
||||
sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.7"
|
||||
version: "4.0.9"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -21,10 +37,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: async
|
||||
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
||||
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.13.0"
|
||||
version: "2.13.1"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -49,6 +65,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.4"
|
||||
cli_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cli_config
|
||||
sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.0"
|
||||
cli_util:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -65,6 +89,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.2"
|
||||
code_assets:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: code_assets
|
||||
sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -73,6 +105,22 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.1"
|
||||
convert:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: convert
|
||||
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
coverage:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: coverage
|
||||
sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.15.0"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -85,18 +133,18 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: cupertino_icons
|
||||
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
|
||||
sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.8"
|
||||
version: "1.0.9"
|
||||
equatable:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: equatable
|
||||
sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7"
|
||||
sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.7"
|
||||
version: "2.0.8"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -109,10 +157,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
|
||||
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
version: "2.2.0"
|
||||
ffi_leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi_leak_tracker
|
||||
sha256: "4093d4ef9ca06ffe2786e73bfb25e22aa92112b9bb4ec941f11e3e6b61489a97"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.1.2"
|
||||
file:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -125,10 +181,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: fl_chart
|
||||
sha256: "74959b99b92b9eebeed1a4049426fd67c4abc3c5a0f4d12e2877097d6a11ae08"
|
||||
sha256: b938f77d042cbcd822936a7a359a7235bad8bd72070de1f827efc2cc297ac888
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.69.2"
|
||||
version: "1.2.0"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
|
|
@ -154,10 +210,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_riverpod
|
||||
sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1"
|
||||
sha256: "4e166be88e1dbbaa34a280bdb744aeae73b7ef25fdf8db7a3bb776760a3648e2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.1"
|
||||
version: "3.3.1"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
|
|
@ -168,22 +224,38 @@ packages:
|
|||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
frontend_server_client:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: frontend_server_client
|
||||
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.0"
|
||||
glob:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: glob
|
||||
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
go_router:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: go_router
|
||||
sha256: f02fd7d2a4dc512fec615529824fdd217fecb3a3d3de68360293a551f21634b3
|
||||
sha256: "92d8cee7c57dff0a6c409c05597b460002434eccf7424a712283225b3962d03f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "14.8.1"
|
||||
version: "17.2.3"
|
||||
google_fonts:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: google_fonts
|
||||
sha256: ba03d03bcaa2f6cb7bd920e3b5027181db75ab524f8891c8bc3aa603885b8055
|
||||
sha256: "4e9391085e524954a51e3625b7c9c7e9851dc3f376603208bb45c24b9a66255d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.3"
|
||||
version: "8.1.0"
|
||||
google_identity_services_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -204,18 +276,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: google_sign_in_android
|
||||
sha256: "5ec98ab35387c68c0050495bb211bd88375873723a80fae7c2e9266ea0bdd8bb"
|
||||
sha256: be0d0733a6a7c5da165879d844a239aa87587a3c767a9163faedde581f731f76
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.2.7"
|
||||
version: "7.2.10"
|
||||
google_sign_in_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: google_sign_in_ios
|
||||
sha256: aee8bb29484611a37607849175a2009d423de6bd7c129326c69fd7068f0b6ed7
|
||||
sha256: ac1e4c1205267cb7999d1d81333fccffdfda29e853f434bbaf71525498bb6950
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.2.4"
|
||||
version: "6.3.0"
|
||||
google_sign_in_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -228,10 +300,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: google_sign_in_web
|
||||
sha256: "2fc1f941e6443b2d6984f4056a727a3eaeab15d8ee99ba7125d79029be75a1da"
|
||||
sha256: d473003eeca892f96a01a64fc803378be765071cb0c265ee872c7f8683245d14
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
version: "1.1.3"
|
||||
hooks:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: hooks
|
||||
sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
http:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -240,6 +320,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.6.0"
|
||||
http_multi_server:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http_multi_server
|
||||
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -252,26 +340,50 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: image
|
||||
sha256: "51555e36056541237b15b57afc31a0f53d4f9aefd9bd00873a6dc0090e54e332"
|
||||
sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.6.0"
|
||||
version: "4.8.0"
|
||||
intl:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: intl
|
||||
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
|
||||
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.19.0"
|
||||
version: "0.20.2"
|
||||
io:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: io
|
||||
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
jni:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: jni
|
||||
sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
jni_flutter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: jni_flutter
|
||||
sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
json_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: json_annotation
|
||||
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
|
||||
sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.9.0"
|
||||
version: "4.11.0"
|
||||
jwt_decoder:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -308,10 +420,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: lints
|
||||
sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0
|
||||
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
version: "6.1.0"
|
||||
logging:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -352,22 +464,62 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: mime
|
||||
sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
native_toolchain_c:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: native_toolchain_c
|
||||
sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.17.6"
|
||||
node_preamble:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: node_preamble
|
||||
sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
objective_c:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: objective_c
|
||||
sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.3.0"
|
||||
package_config:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_config
|
||||
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
package_info_plus:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968"
|
||||
sha256: "4bf625947f6c7713ee242296a682e23e44823c09cf9d79e4f1238923c92db852"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "8.3.1"
|
||||
version: "10.1.0"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: package_info_plus_platform_interface
|
||||
sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
|
||||
sha256: db762cb2f4f25ee60fb6359773861b0f199e00b90d237bd85a76a1e806b46ef4
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
version: "4.1.0"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -388,18 +540,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e
|
||||
sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.22"
|
||||
version: "2.3.1"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4"
|
||||
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.1"
|
||||
version: "2.6.0"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -428,10 +580,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1"
|
||||
sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
version: "7.0.2"
|
||||
platform:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -448,38 +600,62 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
pool:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pool
|
||||
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.2"
|
||||
posix:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: posix
|
||||
sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61"
|
||||
sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.3"
|
||||
version: "6.5.0"
|
||||
pub_semver:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pub_semver
|
||||
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
record_use:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: record_use
|
||||
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.0"
|
||||
riverpod:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: riverpod
|
||||
sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959"
|
||||
sha256: "8c22216be8ad3ef2b44af3a329693558c98eca7b8bd4ef495c92db0bba279f83"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.1"
|
||||
version: "3.2.1"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences
|
||||
sha256: "2939ae520c9024cb197fc20dee269cd8cdbf564c8b5746374ec6cacdc5169e64"
|
||||
sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.5.4"
|
||||
version: "2.5.5"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: "83af5c682796c0f7719c2bbf74792d113e40ae97981b8f266fa84574573556bc"
|
||||
sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.18"
|
||||
version: "2.4.23"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -500,10 +676,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_platform_interface
|
||||
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
|
||||
sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
version: "2.4.2"
|
||||
shared_preferences_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -520,6 +696,38 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf
|
||||
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.2"
|
||||
shelf_packages_handler:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_packages_handler
|
||||
sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
shelf_static:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_static
|
||||
sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.3"
|
||||
shelf_web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shelf_web_socket
|
||||
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
simple_gesture_detector:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -533,14 +741,30 @@ packages:
|
|||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
source_map_stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_map_stack_trace
|
||||
sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.2"
|
||||
source_maps:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_maps
|
||||
sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.10.13"
|
||||
source_span:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: source_span
|
||||
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
|
||||
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.10.1"
|
||||
version: "1.10.2"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -577,10 +801,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: table_calendar
|
||||
sha256: b2896b7c86adf3a4d9c911d860120fe3dbe03c85db43b22fd61f14ee78cdbb63
|
||||
sha256: "0c0c6219878b363a2d5f40c7afb159d845f253d061dc3c822aa0d5fe0f721982"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.3"
|
||||
version: "3.2.0"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -589,6 +813,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.2"
|
||||
test:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test
|
||||
sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.30.0"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -597,6 +829,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.10"
|
||||
test_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_core
|
||||
sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.16"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -617,18 +857,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: "767344bf3063897b5cf0db830e94f904528e6dd50a6dfaf839f0abf509009611"
|
||||
sha256: "3bb000251e55d4a209aa0e2e563309dc9bb2befea2295fd0cec1f51760aac572"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.28"
|
||||
version: "6.3.29"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: cfde38aa257dae62ffe79c87fab20165dfdf6988c1d31b58ebf59b9106062aad
|
||||
sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.6"
|
||||
version: "6.4.1"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -657,10 +897,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
|
||||
sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
version: "2.4.3"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -681,10 +921,18 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: vm_service
|
||||
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
|
||||
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "15.0.2"
|
||||
version: "15.2.0"
|
||||
watcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: watcher
|
||||
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
web:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -693,14 +941,38 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.1"
|
||||
web_socket:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket
|
||||
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
web_socket_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket_channel
|
||||
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
webkit_inspection_protocol:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webkit_inspection_protocol
|
||||
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.1"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: win32
|
||||
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
|
||||
sha256: a1fc9eb9248baa05dfc12ed5b66e377b3e23f095eec078e0371622b9033810d9
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.15.0"
|
||||
version: "6.2.0"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -726,5 +998,5 @@ packages:
|
|||
source: hosted
|
||||
version: "3.1.3"
|
||||
sdks:
|
||||
dart: ">=3.9.0 <4.0.0"
|
||||
flutter: ">=3.35.0"
|
||||
dart: ">=3.10.3 <4.0.0"
|
||||
flutter: ">=3.38.4"
|
||||
|
|
|
|||
|
|
@ -15,31 +15,31 @@ dependencies:
|
|||
|
||||
http: ^1.1.0
|
||||
google_sign_in: ^7.1.1
|
||||
shared_preferences: ^2.2.2
|
||||
shared_preferences: ^2.5.5
|
||||
jwt_decoder: ^2.0.1
|
||||
package_info_plus: ^8.0.2
|
||||
package_info_plus: ^10.1.0
|
||||
# Icons
|
||||
cupertino_icons: ^1.0.8
|
||||
cupertino_icons: ^1.0.9
|
||||
lucide_icons: ^0.257.0
|
||||
|
||||
# Navigation
|
||||
go_router: ^14.6.0
|
||||
go_router: ^17.2.3
|
||||
|
||||
# State Management
|
||||
flutter_riverpod: ^2.6.1
|
||||
flutter_riverpod: ^3.3.1
|
||||
|
||||
# Charts
|
||||
fl_chart: ^0.69.0
|
||||
fl_chart: ^1.2.0
|
||||
|
||||
# Calendar
|
||||
table_calendar: ^3.1.2
|
||||
table_calendar: ^3.2.0
|
||||
|
||||
# Utilities
|
||||
intl: ^0.19.0
|
||||
intl: ^0.20.2
|
||||
url_launcher: ^6.3.1
|
||||
|
||||
# Google Fonts for distinctive typography
|
||||
google_fonts: ^6.2.1
|
||||
google_fonts: ^8.1.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
@ -67,4 +67,3 @@ flutter:
|
|||
# To add assets to your application, add an assets section, like this:
|
||||
assets:
|
||||
- assets/icon/
|
||||
- assets/images/
|
||||
|
|
|
|||
Loading…
Reference in a new issue