import 'dart:convert';
import 'package:flutter/foundation.dart';
import '../../../core/auth/auth_store.dart';
import '../../../core/storage/local_storage.dart';
import '../../../core/sync/master_data_sync_repository.dart';
import '../../customers/data/customer_store.dart';
import '../../../core/web/web_office_api.dart';
class Job {
const Job({
required this.id,
required this.number,
required this.customer,
required this.site,
required this.address,
this.customerId,
this.siteId,
this.rowVersion = 0,
this.syncStatus = EntitySyncStatus.localOnly,
this.syncError,
});
final String id;
final String number;
final String customer;
final String site;
final String address;
final String? customerId;
final String? siteId;
final int rowVersion;
final EntitySyncStatus syncStatus;
final String? syncError;
Map<String, Object?> toJson() => {
'id': id,
'number': number,
'customer': customer,
'site': site,
'address': address,
'customerId': customerId ?? '',
'siteId': siteId ?? '',
'row_version': rowVersion,
'sync_status': syncStatus.value,
'sync_error': syncError,
};
factory Job.fromJson(Map<String, dynamic> json) => Job(
id: json['id'] as String,
number: json['number'] as String,
customer: json['customer'] as String,
site: json['site'] as String,
address: json['address'] as String? ?? '',
customerId: (json['customerId'] as String?)?.isEmpty ?? true
? null
: json['customerId'] as String,
siteId: (json['siteId'] as String?)?.isEmpty ?? true
? null
: json['siteId'] as String,
rowVersion: json['row_version'] as int? ?? 0,
syncStatus: EntitySyncStatusValue.parse(json['sync_status']),
syncError: json['sync_error'] as String?,
);
Job withLinks({String? customerId, String? siteId}) => Job(
id: id,
number: number,
customer: customer,
site: site,
address: address,
customerId: customerId ?? this.customerId,
siteId: siteId ?? this.siteId,
rowVersion: rowVersion,
syncStatus: syncStatus,
syncError: syncError,
);
}
class JobStore extends ChangeNotifier {
JobStore._();
static final instance = JobStore._();
static const _key = 'machweis_jobs_v1';
final List<Job> _jobs = [];
List<Job> get jobs => List.unmodifiable(_jobs);
Future<void> initialize() async {
if (kIsWeb && AuthStore.instance.session != null) {
try {
final rows = await WebOfficeApi.instance.listJobs();
_jobs
..clear()
..addAll(rows.map(Job.fromJson));
} on Object {
// Keep the last loaded web snapshot visible on transient errors.
}
notifyListeners();
return;
}
if (LocalStorage.instance.usesRelationalStore) {
final repository = LocalJobRepository();
final stored = await repository.readAll();
if (stored.isNotEmpty || await repository.hasAnyRows()) {
_jobs
..clear()
..addAll(stored);
notifyListeners();
return;
}
if (AuthStore.instance.session != null) {
_jobs.clear();
notifyListeners();
return;
}
await _migrateLegacy(repository);
notifyListeners();
return;
}
final stored = await LocalStorage.instance.getString(_key);
if (stored == null) return;
try {
final values = jsonDecode(stored) as List<dynamic>;
_jobs
..clear()
..addAll(
values.map((value) => Job.fromJson(value as Map<String, dynamic>)),
);
} on FormatException {
await LocalStorage.instance.remove(_key);
}
}
Future<void> add(Job job) async {
if (kIsWeb) {
await WebOfficeApi.instance.createJob({
'customerId': job.customerId ?? '',
'siteId': job.siteId ?? '',
'number': job.number,
'title': job.site,
'description': '',
'status': 'open',
});
await initialize();
return;
}
_jobs.insert(0, job);
notifyListeners();
await _persist(job);
if (LocalStorage.instance.usesRelationalStore) await initialize();
notifyListeners();
}
Future<void> remove(String id) async {
if (kIsWeb) {
await WebOfficeApi.instance.deleteJob(id);
await initialize();
return;
}
_jobs.removeWhere((job) => job.id == id);
notifyListeners();
if (LocalStorage.instance.usesRelationalStore) {
await LocalJobRepository().remove(id);
} else {
await _persist();
}
notifyListeners();
}
Future<void> _migrateLegacy(LocalJobRepository repository) async {
final stored = await LocalStorage.instance.getString(_key);
if (stored == null) return;
try {
final values = jsonDecode(stored) as List<dynamic>;
final migrated = values
.map((value) => Job.fromJson(value as Map<String, dynamic>))
.map((job) {
final matches = CustomerStore.instance.customers
.where(
(customer) => customer.name.trim() == job.customer.trim(),
)
.toList();
return job.withLinks(
customerId: matches.length == 1 ? matches.single.id : null,
siteId: job.siteId ?? '${job.id}-site',
);
})
.toList();
for (final job in migrated) {
await repository.save(job, legacy: true);
}
_jobs
..clear()
..addAll(migrated);
} on Object {
// Legacy data is retained for a later recovery attempt.
}
}
Future<void> _persist([Job? job]) async {
if (LocalStorage.instance.usesRelationalStore) {
if (job != null) await LocalJobRepository().save(job);
return;
}
await LocalStorage.instance.setString(
_key,
jsonEncode(_jobs.map((job) => job.toJson()).toList()),
);
}
}
class LocalJobRepository {
final SqliteMasterDataSyncRepository _sync = SqliteMasterDataSyncRepository();
Future<List<Job>> readAll() async {
final tenant = AuthStore.instance.session?.tenantId;
return (await LocalStorage.instance.mobileQuery(
'jobs',
where: tenant == null
? 'deleted_at IS NULL AND tenant_id IS NULL'
: 'deleted_at IS NULL AND tenant_id = ?',
whereArgs: tenant == null ? null : [tenant],
orderBy: 'updated_at DESC',
))
.map(
(row) => Job.fromJson({
...row,
'customerId': row['customer_id'],
'siteId': row['site_id'],
'customer': row['customer_name'],
'site': row['site_name'],
}),
)
.toList();
}
Future<bool> hasAnyRows() async {
final tenant = AuthStore.instance.session?.tenantId;
return (await LocalStorage.instance.mobileQuery(
'jobs',
where: tenant == null ? 'tenant_id IS NULL' : 'tenant_id = ?',
whereArgs: tenant == null ? null : [tenant],
)).isNotEmpty;
}
Future<void> save(Job job, {bool legacy = false}) async {
final siteId = job.siteId;
final now = DateTime.now().toUtc().toIso8601String();
final siteRow = siteId == null
? null
: <String, Object?>{
'id': siteId,
'customer_id': job.customerId,
'name': job.site,
'address': job.address,
'updated_at': now,
};
final jobRow = <String, Object?>{
'id': job.id,
'number': job.number,
'title': job.site,
'description': '',
'status': 'open',
'customer_id': job.customerId,
'site_id': siteId,
'customer_name': job.customer,
'site_name': job.site,
'address': job.address,
'updated_at': now,
};
if (legacy) {
if (siteRow != null) await _sync.saveLegacy('sites', siteRow);
await _sync.saveLegacy('jobs', jobRow);
return;
}
if (job.siteId != null) {
await _sync.saveJobWithSite(
tenantId: AuthStore.instance.session?.tenantId,
siteRow: siteRow!,
sitePayload: {
'customer_id': job.customerId ?? '',
'name': job.site,
'address': job.address,
},
jobRow: jobRow,
jobPayload: {
'customer_id': job.customerId ?? '',
'site_id': siteId,
'number': job.number,
'title': job.site,
'description': '',
'status': 'open',
},
);
return;
}
await _sync.saveLocalChange(
tenantId: AuthStore.instance.session?.tenantId,
entityType: 'jobs',
row: jobRow,
payload: {
'customer_id': job.customerId ?? '',
'number': job.number,
'title': job.site,
'description': '',
'status': 'open',
},
);
}
Future<void> remove(String id) => _sync.deleteLocal(
tenantId: AuthStore.instance.session?.tenantId,
entityType: 'jobs',
entityId: id,
);
}
