mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-19 04:40:32 -05:00
da85aedefb
Implement the missing Flutter data layer so release builds compile: Dio ApiClient for /api/v1 (timer, time entries, projects, tasks, finance, time-off, users/me), JSON models, Hive LocalStorage, and offline SyncService queue. Add OpenTelemetry (opentelemetry package) with initMobileOpenTelemetry() reading OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_TOKEN via --dart-define, matching server OTLP base URL and Basic auth behavior. Instrument login token validation, timer start/stop, and sync pending. Fix SyncUseCase to import storage SyncService, use trusted insecure hosts, and call syncAll(). GitHub Actions (build-mobile.yml, cd-release.yml): run flutter test; pass OTLP secrets into flutter build apk/appbundle/ios; switch iOS CI to release simulator builds and package build/ios/iphonesimulator/Runner.app to avoid requiring an Apple Development Team for generic device builds. .gitignore: allow tracking mobile/lib/data/ despite the repo-wide data/ ignore rule.
95 lines
2.6 KiB
Dart
95 lines
2.6 KiB
Dart
import 'package:timetracker_mobile/data/api/api_client.dart';
|
|
import 'package:timetracker_mobile/data/models/time_entry.dart';
|
|
import 'package:timetracker_mobile/data/storage/local_storage.dart';
|
|
|
|
class SyncService {
|
|
SyncService(this._api);
|
|
|
|
final ApiClient? _api;
|
|
|
|
static Future<void> queueCreateTimeEntry({
|
|
required int projectId,
|
|
int? taskId,
|
|
required String startTime,
|
|
String? endTime,
|
|
String? notes,
|
|
String? tags,
|
|
bool? billable,
|
|
}) async {
|
|
final q = await LocalStorage.getSyncQueue();
|
|
q.add({
|
|
'op': 'create_time_entry',
|
|
'project_id': projectId,
|
|
if (taskId != null) 'task_id': taskId,
|
|
'start_time': startTime,
|
|
if (endTime != null) 'end_time': endTime,
|
|
if (notes != null) 'notes': notes,
|
|
if (tags != null) 'tags': tags,
|
|
if (billable != null) 'billable': billable,
|
|
});
|
|
await LocalStorage.setSyncQueue(q);
|
|
}
|
|
|
|
static Future<void> queueDeleteTimeEntry(int entryId) async {
|
|
final q = await LocalStorage.getSyncQueue();
|
|
q.add({
|
|
'op': 'delete_time_entry',
|
|
'entry_id': entryId,
|
|
});
|
|
await LocalStorage.setSyncQueue(q);
|
|
}
|
|
|
|
Future<void> syncAll() async {
|
|
await processQueue();
|
|
await syncFromServer();
|
|
}
|
|
|
|
Future<void> processQueue() async {
|
|
final api = _api;
|
|
if (api == null) return;
|
|
|
|
final q = await LocalStorage.getSyncQueue();
|
|
final remaining = <Map<String, dynamic>>[];
|
|
|
|
for (final op in q) {
|
|
final type = op['op']?.toString();
|
|
try {
|
|
if (type == 'create_time_entry') {
|
|
await api.createTimeEntry(
|
|
projectId: (op['project_id'] as num).toInt(),
|
|
taskId: (op['task_id'] as num?)?.toInt(),
|
|
startTime: op['start_time'].toString(),
|
|
endTime: op['end_time']?.toString(),
|
|
notes: op['notes']?.toString(),
|
|
tags: op['tags']?.toString(),
|
|
billable: op['billable'] as bool?,
|
|
);
|
|
} else if (type == 'delete_time_entry') {
|
|
await api.deleteTimeEntry((op['entry_id'] as num).toInt());
|
|
} else {
|
|
remaining.add(op);
|
|
}
|
|
} catch (_) {
|
|
remaining.add(op);
|
|
}
|
|
}
|
|
|
|
await LocalStorage.setSyncQueue(remaining);
|
|
}
|
|
|
|
Future<void> syncFromServer() async {
|
|
final api = _api;
|
|
if (api == null) return;
|
|
try {
|
|
final res = await api.getTimeEntries(perPage: 200);
|
|
final raw = res['time_entries'] as List<dynamic>? ?? [];
|
|
for (final e in raw) {
|
|
if (e is Map) {
|
|
final entry = TimeEntry.fromJson(Map<String, dynamic>.from(e));
|
|
await LocalStorage.saveTimeEntry(entry);
|
|
}
|
|
}
|
|
} catch (_) {}
|
|
}
|
|
}
|