mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-18 04:08:48 -05:00
feat(mobile): add finance/workforce screen and providers
- Add FinanceWorkforceScreen and finance_workforce_providers - Update home_screen with IndexedStack tab state; api_client updates - Align mobile with web/desktop for workforce and finance
This commit is contained in:
@@ -183,6 +183,20 @@ class ApiClient {
|
||||
return response.data as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
/// Get clients
|
||||
Future<Map<String, dynamic>> getClients({
|
||||
String? status,
|
||||
int? page,
|
||||
int? perPage,
|
||||
}) async {
|
||||
final queryParams = <String, dynamic>{};
|
||||
if (status != null) queryParams['status'] = status;
|
||||
if (page != null) queryParams['page'] = page;
|
||||
if (perPage != null) queryParams['per_page'] = perPage;
|
||||
final response = await _dio.get('/api/v1/clients', queryParameters: queryParams);
|
||||
return response.data as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
// ==================== Task Operations ====================
|
||||
|
||||
/// Get tasks
|
||||
@@ -207,4 +221,160 @@ class ApiClient {
|
||||
final response = await _dio.get('/api/v1/tasks/$taskId');
|
||||
return response.data as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
// ==================== Freelancer Cashflow Parity ====================
|
||||
|
||||
Future<Map<String, dynamic>> getInvoices({
|
||||
String? status,
|
||||
int? clientId,
|
||||
int? projectId,
|
||||
int? page,
|
||||
int? perPage,
|
||||
}) async {
|
||||
final queryParams = <String, dynamic>{};
|
||||
if (status != null) queryParams['status'] = status;
|
||||
if (clientId != null) queryParams['client_id'] = clientId;
|
||||
if (projectId != null) queryParams['project_id'] = projectId;
|
||||
if (page != null) queryParams['page'] = page;
|
||||
if (perPage != null) queryParams['per_page'] = perPage;
|
||||
final response = await _dio.get('/api/v1/invoices', queryParameters: queryParams);
|
||||
return response.data as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getInvoice(int invoiceId) async {
|
||||
final response = await _dio.get('/api/v1/invoices/$invoiceId');
|
||||
return response.data as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> createInvoice(Map<String, dynamic> data) async {
|
||||
final response = await _dio.post('/api/v1/invoices', data: data);
|
||||
return response.data as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> updateInvoice(int invoiceId, Map<String, dynamic> data) async {
|
||||
final response = await _dio.put('/api/v1/invoices/$invoiceId', data: data);
|
||||
return response.data as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getExpenses({
|
||||
int? projectId,
|
||||
String? category,
|
||||
String? startDate,
|
||||
String? endDate,
|
||||
int? page,
|
||||
int? perPage,
|
||||
}) async {
|
||||
final queryParams = <String, dynamic>{};
|
||||
if (projectId != null) queryParams['project_id'] = projectId;
|
||||
if (category != null) queryParams['category'] = category;
|
||||
if (startDate != null) queryParams['start_date'] = startDate;
|
||||
if (endDate != null) queryParams['end_date'] = endDate;
|
||||
if (page != null) queryParams['page'] = page;
|
||||
if (perPage != null) queryParams['per_page'] = perPage;
|
||||
final response = await _dio.get('/api/v1/expenses', queryParameters: queryParams);
|
||||
return response.data as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> createExpense(Map<String, dynamic> data) async {
|
||||
final response = await _dio.post('/api/v1/expenses', data: data);
|
||||
return response.data as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getCapacityReport({required String startDate, required String endDate}) async {
|
||||
final response = await _dio.get(
|
||||
'/api/v1/reports/capacity',
|
||||
queryParameters: {
|
||||
'start_date': startDate,
|
||||
'end_date': endDate,
|
||||
},
|
||||
);
|
||||
return response.data as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getTimesheetPeriods({String? status, String? startDate, String? endDate}) async {
|
||||
final queryParams = <String, dynamic>{};
|
||||
if (status != null) queryParams['status'] = status;
|
||||
if (startDate != null) queryParams['start_date'] = startDate;
|
||||
if (endDate != null) queryParams['end_date'] = endDate;
|
||||
final response = await _dio.get('/api/v1/timesheet-periods', queryParameters: queryParams);
|
||||
return response.data as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> submitTimesheetPeriod(int periodId) async {
|
||||
final response = await _dio.post('/api/v1/timesheet-periods/$periodId/submit');
|
||||
return response.data as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> approveTimesheetPeriod(int periodId, {String? comment}) async {
|
||||
final data = <String, dynamic>{};
|
||||
if (comment != null && comment.trim().isNotEmpty) data['comment'] = comment.trim();
|
||||
final response = await _dio.post('/api/v1/timesheet-periods/$periodId/approve', data: data);
|
||||
return response.data as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> rejectTimesheetPeriod(int periodId, {String? reason}) async {
|
||||
final data = <String, dynamic>{};
|
||||
if (reason != null && reason.trim().isNotEmpty) data['reason'] = reason.trim();
|
||||
final response = await _dio.post('/api/v1/timesheet-periods/$periodId/reject', data: data);
|
||||
return response.data as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getLeaveTypes() async {
|
||||
final response = await _dio.get('/api/v1/time-off/leave-types');
|
||||
return response.data as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getTimeOffRequests({
|
||||
String? status,
|
||||
String? startDate,
|
||||
String? endDate,
|
||||
}) async {
|
||||
final queryParams = <String, dynamic>{};
|
||||
if (status != null) queryParams['status'] = status;
|
||||
if (startDate != null) queryParams['start_date'] = startDate;
|
||||
if (endDate != null) queryParams['end_date'] = endDate;
|
||||
final response = await _dio.get('/api/v1/time-off/requests', queryParameters: queryParams);
|
||||
return response.data as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> createTimeOffRequest({
|
||||
required int leaveTypeId,
|
||||
required String startDate,
|
||||
required String endDate,
|
||||
double? requestedHours,
|
||||
String? comment,
|
||||
bool submit = true,
|
||||
}) async {
|
||||
final data = <String, dynamic>{
|
||||
'leave_type_id': leaveTypeId,
|
||||
'start_date': startDate,
|
||||
'end_date': endDate,
|
||||
'submit': submit,
|
||||
};
|
||||
if (requestedHours != null) data['requested_hours'] = requestedHours;
|
||||
if (comment != null && comment.trim().isNotEmpty) data['comment'] = comment.trim();
|
||||
final response = await _dio.post('/api/v1/time-off/requests', data: data);
|
||||
return response.data as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getTimeOffBalances({int? userId}) async {
|
||||
final queryParams = <String, dynamic>{};
|
||||
if (userId != null) queryParams['user_id'] = userId;
|
||||
final response = await _dio.get('/api/v1/time-off/balances', queryParameters: queryParams);
|
||||
return response.data as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> approveTimeOffRequest(int requestId, {String? comment}) async {
|
||||
final data = <String, dynamic>{};
|
||||
if (comment != null && comment.trim().isNotEmpty) data['comment'] = comment.trim();
|
||||
final response = await _dio.post('/api/v1/time-off/requests/$requestId/approve', data: data);
|
||||
return response.data as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> rejectTimeOffRequest(int requestId, {String? comment}) async {
|
||||
final data = <String, dynamic>{};
|
||||
if (comment != null && comment.trim().isNotEmpty) data['comment'] = comment.trim();
|
||||
final response = await _dio.post('/api/v1/time-off/requests/$requestId/reject', data: data);
|
||||
return response.data as Map<String, dynamic>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:timetracker_mobile/presentation/providers/api_provider.dart';
|
||||
|
||||
/// Date range for capacity/periods (current week)
|
||||
({String start, String end}) _weekRange() {
|
||||
final now = DateTime.now();
|
||||
final start = DateTime(now.year, now.month, now.day - now.weekday + 1);
|
||||
final end = start.add(const Duration(days: 6));
|
||||
return (start: start.toIso8601String().split('T')[0], end: end.toIso8601String().split('T')[0]);
|
||||
}
|
||||
|
||||
final financeInvoicesProvider = FutureProvider.family<Map<String, dynamic>, int>((ref, page) async {
|
||||
final client = await ref.watch(apiClientProvider.future);
|
||||
if (client == null) return {'invoices': <Map<String, dynamic>>[], 'pagination': {'page': 1, 'pages': 1}};
|
||||
return client.getInvoices(page: page, perPage: 20);
|
||||
});
|
||||
|
||||
final financeExpensesProvider = FutureProvider.family<Map<String, dynamic>, int>((ref, page) async {
|
||||
final client = await ref.watch(apiClientProvider.future);
|
||||
if (client == null) return {'expenses': <Map<String, dynamic>>[], 'pagination': {'page': 1, 'pages': 1}};
|
||||
return client.getExpenses(page: page, perPage: 20);
|
||||
});
|
||||
|
||||
final timesheetPeriodsProvider = FutureProvider<Map<String, dynamic>>((ref) async {
|
||||
final client = await ref.watch(apiClientProvider.future);
|
||||
if (client == null) return {'timesheet_periods': <Map<String, dynamic>>[]};
|
||||
final range = _weekRange();
|
||||
return client.getTimesheetPeriods(startDate: range.start, endDate: range.end);
|
||||
});
|
||||
|
||||
final capacityReportProvider = FutureProvider<Map<String, dynamic>>((ref) async {
|
||||
final client = await ref.watch(apiClientProvider.future);
|
||||
if (client == null) return {'capacity': <Map<String, dynamic>>[]};
|
||||
final range = _weekRange();
|
||||
return client.getCapacityReport(startDate: range.start, endDate: range.end);
|
||||
});
|
||||
|
||||
final timeOffRequestsProvider = FutureProvider<Map<String, dynamic>>((ref) async {
|
||||
final client = await ref.watch(apiClientProvider.future);
|
||||
if (client == null) return {'time_off_requests': <Map<String, dynamic>>[]};
|
||||
return client.getTimeOffRequests();
|
||||
});
|
||||
|
||||
final leaveBalancesProvider = FutureProvider<Map<String, dynamic>>((ref) async {
|
||||
final client = await ref.watch(apiClientProvider.future);
|
||||
if (client == null) return {'balances': <Map<String, dynamic>>[]};
|
||||
return client.getTimeOffBalances();
|
||||
});
|
||||
|
||||
final leaveTypesProvider = FutureProvider<Map<String, dynamic>>((ref) async {
|
||||
final client = await ref.watch(apiClientProvider.future);
|
||||
if (client == null) return {'leave_types': <Map<String, dynamic>>[]};
|
||||
return client.getLeaveTypes();
|
||||
});
|
||||
|
||||
final financeProjectsProvider = FutureProvider<Map<String, dynamic>>((ref) async {
|
||||
final client = await ref.watch(apiClientProvider.future);
|
||||
if (client == null) return {'projects': <Map<String, dynamic>>[]};
|
||||
return client.getProjects(status: 'active', perPage: 100);
|
||||
});
|
||||
|
||||
final financeClientsProvider = FutureProvider<Map<String, dynamic>>((ref) async {
|
||||
final client = await ref.watch(apiClientProvider.future);
|
||||
if (client == null) return {'clients': <Map<String, dynamic>>[]};
|
||||
return client.getClients(status: 'active', perPage: 100);
|
||||
});
|
||||
|
||||
/// Whether the current user can approve timesheets / time-off
|
||||
final userCanApproveProvider = FutureProvider<bool>((ref) async {
|
||||
final client = await ref.watch(apiClientProvider.future);
|
||||
if (client == null) return false;
|
||||
final me = await client.getUsersMe();
|
||||
final user = me['user'] is Map<String, dynamic> ? me['user'] as Map<String, dynamic> : <String, dynamic>{};
|
||||
final role = (user['role'] ?? '').toString().toLowerCase();
|
||||
return (user['is_admin'] == true) || (role == 'admin' || role == 'owner' || role == 'manager' || role == 'approver');
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,7 @@ import 'timer_screen.dart';
|
||||
import 'projects_screen.dart';
|
||||
import 'time_entries_screen.dart';
|
||||
import 'settings_screen.dart';
|
||||
import 'finance_workforce_screen.dart';
|
||||
import 'dart:async';
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
@@ -28,13 +29,17 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
const DashboardTab(),
|
||||
const ProjectsScreen(),
|
||||
const TimeEntriesScreen(),
|
||||
const FinanceWorkforceScreen(),
|
||||
const SettingsScreen(),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: _screens[_currentIndex],
|
||||
body: IndexedStack(
|
||||
index: _currentIndex,
|
||||
children: _screens,
|
||||
),
|
||||
bottomNavigationBar: NavigationBar(
|
||||
selectedIndex: _currentIndex,
|
||||
onDestinationSelected: (index) {
|
||||
@@ -58,6 +63,11 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
selectedIcon: Icon(Icons.history),
|
||||
label: 'Entries',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.account_balance_wallet_outlined),
|
||||
selectedIcon: Icon(Icons.account_balance_wallet),
|
||||
label: 'Finance',
|
||||
),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.settings_outlined),
|
||||
selectedIcon: Icon(Icons.settings),
|
||||
|
||||
Reference in New Issue
Block a user