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 '../../../core/web/web_office_api.dart';
enum EmployeeRole { owner, office, supervisor, technician }
extension EmployeeRoleLabel on EmployeeRole {
String get label => switch (this) {
EmployeeRole.owner => 'Inhaber',
EmployeeRole.office => 'Büro',
EmployeeRole.supervisor => 'Bauleiter',
EmployeeRole.technician => 'Monteur',
};
}
class Employee {
const Employee({
required this.id,
required this.name,
required this.email,
required this.role,
this.active = true,
this.employeeNumber = '',
this.phone = '',
this.rowVersion = 0,
this.syncStatus = EntitySyncStatus.localOnly,
this.syncError,
});
final String id;
final String name;
final String email;
final EmployeeRole role;
final bool active;
final String employeeNumber;
final String phone;
final int rowVersion;
final EntitySyncStatus syncStatus;
final String? syncError;
Map<String, Object?> toJson() => {
'id': id,
'name': name,
'email': email,
'role': role.name,
'active': active,
'employeeNumber': employeeNumber,
'phone': phone,
'row_version': rowVersion,
'sync_status': syncStatus.value,
'sync_error': syncError,
};
factory Employee.fromJson(Map<String, dynamic> json) {
final firstName = json['first_name'] as String? ?? '';
final lastName = json['last_name'] as String? ?? '';
final active = json['active'] as bool?;
return Employee(
id: json['id'] as String,
name: json['name'] as String? ?? '$firstName $lastName'.trim(),
email: json['email'] as String? ?? '',
role: EmployeeRole.values.firstWhere(
(role) => role.name == json['role'],
orElse: () => EmployeeRole.technician,
),
active: active ?? (json['is_active'] == 1 || json['is_active'] == true),
employeeNumber:
json['employeeNumber'] as String? ??
json['employee_number'] as String? ??
'',
phone: json['phone'] as String? ?? '',
rowVersion: json['row_version'] as int? ?? 0,
syncStatus: EntitySyncStatusValue.parse(json['sync_status']),
syncError: json['sync_error'] as String?,
);
}
}
class EmployeeStore extends ChangeNotifier {
EmployeeStore._();
static final instance = EmployeeStore._();
static const _key = 'machweis_employees_v1';
final List<Employee> _employees = [];
List<Employee> get employees => List.unmodifiable(_employees);
List<Employee> get activeEmployees =>
_employees.where((employee) => employee.active).toList();
Future<void> initialize() async {
if (kIsWeb && AuthStore.instance.session != null) {
try {
final rows = await WebOfficeApi.instance.listEmployees();
_employees
..clear()
..addAll(rows.map(Employee.fromJson));
} on Object {
// Keep the last loaded web snapshot visible on transient errors.
}
notifyListeners();
return;
}
if (LocalStorage.instance.usesRelationalStore) {
final repository = LocalEmployeeRepository();
final relational = await repository.readAll();
if (relational.isNotEmpty || await repository.hasAnyRows()) {
_employees
..clear()
..addAll(relational);
notifyListeners();
return;
}
if (AuthStore.instance.session != null) {
_employees.clear();
notifyListeners();
return;
}
final stored = await LocalStorage.instance.getString(_key);
if (stored != null) {
try {
final values = jsonDecode(stored) as List<dynamic>;
final migrated = values
.map((value) => Employee.fromJson(value as Map<String, dynamic>))
.toList();
for (final employee in migrated) {
await repository.save(employee, legacy: true);
}
_employees
..clear()
..addAll(migrated);
notifyListeners();
return;
} on Object {
// Preserve unreadable legacy values for a later recovery attempt.
}
}
const demo = Employee(
id: 'demo-max',
name: 'Max Schneider',
email: 'max@schneider-elektro.de',
role: EmployeeRole.technician,
);
_employees.add(demo);
await repository.save(demo, legacy: true);
notifyListeners();
return;
}
final stored = await LocalStorage.instance.getString(_key);
if (stored == null) {
_employees.add(
const Employee(
id: 'demo-max',
name: 'Max Schneider',
email: 'max@schneider-elektro.de',
role: EmployeeRole.technician,
),
);
await _persist();
return;
}
try {
final values = jsonDecode(stored) as List<dynamic>;
_employees
..clear()
..addAll(
values.map(
(value) => Employee.fromJson(value as Map<String, dynamic>),
),
);
} on FormatException {
await LocalStorage.instance.remove(_key);
}
}
Future<void> save(Employee employee) async {
if (kIsWeb) {
final parts = employee.name.trim().split(RegExp(r'\\s+'));
final payload = <String, dynamic>{
'employeeNumber': employee.employeeNumber.isEmpty
? 'M-${employee.id.substring(0, employee.id.length < 8 ? employee.id.length : 8)}'
: employee.employeeNumber,
'firstName': parts.isEmpty ? '-' : parts.first,
'lastName': parts.length > 1 ? parts.sublist(1).join(' ') : '-',
'email': employee.email,
'phone': employee.phone,
'isActive': employee.active,
};
final exists = _employees.any((item) => item.id == employee.id);
if (exists) {
await WebOfficeApi.instance.updateEmployee(employee.id, payload);
final previous = _employees.firstWhere(
(item) => item.id == employee.id,
);
if (previous.role != employee.role) {
await WebOfficeApi.instance.updateEmployeeRole(
employee.id,
employee.role.name,
);
}
} else {
await WebOfficeApi.instance.createEmployee(payload);
}
await initialize();
return;
}
final index = _employees.indexWhere((item) => item.id == employee.id);
if (index == -1) {
_employees.add(employee);
} else {
_employees[index] = employee;
}
notifyListeners();
await _persist(employee);
if (LocalStorage.instance.usesRelationalStore) await initialize();
notifyListeners();
}
Future<void> setActive(Employee employee, bool active) => save(
Employee(
id: employee.id,
name: employee.name,
email: employee.email,
role: employee.role,
active: active,
employeeNumber: employee.employeeNumber,
phone: employee.phone,
rowVersion: employee.rowVersion,
syncStatus: employee.syncStatus,
syncError: employee.syncError,
),
);
Future<void> _persist([Employee? employee]) async {
if (LocalStorage.instance.usesRelationalStore) {
if (employee != null) await LocalEmployeeRepository().save(employee);
return;
}
await LocalStorage.instance.setString(
_key,
jsonEncode(_employees.map((employee) => employee.toJson()).toList()),
);
}
}
class LocalEmployeeRepository {
final SqliteMasterDataSyncRepository _sync = SqliteMasterDataSyncRepository();
Future<List<Employee>> readAll() async {
final tenant = AuthStore.instance.session?.tenantId;
return (await LocalStorage.instance.mobileQuery(
'employee_profiles',
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(Employee.fromJson).toList();
}
Future<bool> hasAnyRows() async {
final tenant = AuthStore.instance.session?.tenantId;
return (await LocalStorage.instance.mobileQuery(
'employee_profiles',
where: tenant == null ? 'tenant_id IS NULL' : 'tenant_id = ?',
whereArgs: tenant == null ? null : [tenant],
)).isNotEmpty;
}
Future<void> save(Employee employee, {bool legacy = false}) {
final parts = employee.name.trim().split(RegExp(r'\s+'));
final firstName = parts.isEmpty ? '-' : parts.first;
final lastName = parts.length > 1 ? parts.sublist(1).join(' ') : '-';
final employeeNumber = employee.employeeNumber.isNotEmpty
? employee.employeeNumber
: 'M-${employee.id.substring(0, employee.id.length < 8 ? employee.id.length : 8)}';
final row = <String, Object?>{
'id': employee.id,
'employee_number': employeeNumber,
'first_name': firstName,
'last_name': lastName,
'email': employee.email,
'phone': employee.phone,
'role': employee.role.name,
'is_active': employee.active ? 1 : 0,
};
if (legacy) return _sync.saveLegacy('employee_profiles', row);
return _sync.saveLocalChange(
tenantId: AuthStore.instance.session?.tenantId,
entityType: 'employee_profiles',
row: row,
payload: {
'employee_number': employeeNumber,
'first_name': firstName,
'last_name': lastName,
'email': employee.email,
'phone': employee.phone,
'is_active': employee.active,
},
);
}
}
