Frontend first draft
This commit is contained in:
144
frontend/pshared/lib/provider/account.dart
Normal file
144
frontend/pshared/lib/provider/account.dart
Normal file
@@ -0,0 +1,144 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
import 'package:pshared/api/errors/unauthorized.dart';
|
||||
import 'package:pshared/api/requests/signup.dart';
|
||||
import 'package:pshared/models/account/account.dart';
|
||||
import 'package:pshared/provider/exception.dart';
|
||||
import 'package:pshared/provider/resource.dart';
|
||||
import 'package:pshared/service/account.dart';
|
||||
|
||||
|
||||
class AccountProvider extends ChangeNotifier {
|
||||
// The resource now wraps our Account? state along with its loading/error state.
|
||||
Resource<Account?> _resource = Resource(data: null);
|
||||
Resource<Account?> get resource => _resource;
|
||||
|
||||
Account? get account => _resource.data;
|
||||
bool get isLoggedIn => account != null;
|
||||
bool get isLoading => _resource.isLoading;
|
||||
Object? get error => _resource.error;
|
||||
|
||||
// Private helper to update the resource and notify listeners.
|
||||
void _setResource(Resource<Account?> newResource) {
|
||||
_resource = newResource;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
|
||||
Future<Account?> login({
|
||||
required String email,
|
||||
required String password,
|
||||
required String locale,
|
||||
}) async {
|
||||
_setResource(_resource.copyWith(isLoading: true, error: null));
|
||||
try {
|
||||
final acc = await AccountService.login(email, password, locale);
|
||||
_setResource(Resource(data: acc, isLoading: false));
|
||||
return acc;
|
||||
} catch (e) {
|
||||
_setResource(_resource.copyWith(isLoading: false, error: toException(e)));
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Account?> restore() async {
|
||||
_setResource(_resource.copyWith(isLoading: true, error: null));
|
||||
try {
|
||||
final acc = await AccountService.restore();
|
||||
_setResource(Resource(data: acc, isLoading: false));
|
||||
return acc;
|
||||
} catch (e) {
|
||||
_setResource(_resource.copyWith(isLoading: false, error: toException(e)));
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> signup(
|
||||
String name,
|
||||
String login,
|
||||
String password,
|
||||
String locale,
|
||||
String organizationName,
|
||||
String timezone,
|
||||
) async {
|
||||
_setResource(_resource.copyWith(isLoading: true, error: null));
|
||||
try {
|
||||
await AccountService.signup(
|
||||
SignupRequest.build(
|
||||
name: name,
|
||||
login: login.trim().toLowerCase(),
|
||||
password: password,
|
||||
locale: locale,
|
||||
organizationName: organizationName,
|
||||
organizationTimeZone: timezone,
|
||||
),
|
||||
);
|
||||
// Signup might not automatically log in the user,
|
||||
// so we just mark the request as complete.
|
||||
_setResource(_resource.copyWith(isLoading: false));
|
||||
} catch (e) {
|
||||
_setResource(_resource.copyWith(isLoading: false, error: toException(e)));
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
_setResource(_resource.copyWith(isLoading: true, error: null));
|
||||
try {
|
||||
await AccountService.logout();
|
||||
_setResource(Resource(data: null, isLoading: false));
|
||||
} catch (e) {
|
||||
_setResource(_resource.copyWith(isLoading: false, error: toException(e)));
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Account?> update({
|
||||
String? locale,
|
||||
String? avatarUrl,
|
||||
String? notificationFrequency,
|
||||
}) async {
|
||||
if (account == null) throw ErrorUnauthorized();
|
||||
_setResource(_resource.copyWith(isLoading: true, error: null));
|
||||
try {
|
||||
final updated = await AccountService.update(
|
||||
account!.copyWith(
|
||||
avatarUrl: () => avatarUrl ?? account!.avatarUrl,
|
||||
locale: locale ?? account!.locale,
|
||||
),
|
||||
);
|
||||
_setResource(Resource(data: updated, isLoading: false));
|
||||
return updated;
|
||||
} catch (e) {
|
||||
_setResource(_resource.copyWith(isLoading: false, error: toException(e)));
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> changePassword(String oldPassword, String newPassword) async {
|
||||
if (account == null) throw ErrorUnauthorized();
|
||||
_setResource(_resource.copyWith(isLoading: true, error: null));
|
||||
try {
|
||||
final updated = await AccountService.changePassword(oldPassword, newPassword);
|
||||
_setResource(Resource(data: updated, isLoading: false));
|
||||
} catch (e) {
|
||||
_setResource(_resource.copyWith(isLoading: false, error: toException(e)));
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Account?> uploadAvatar(XFile avatarFile) async {
|
||||
if (account == null) throw ErrorUnauthorized();
|
||||
_setResource(_resource.copyWith(isLoading: true, error: null));
|
||||
try {
|
||||
final avatarUrl = await AccountService.uploadAvatar(account!.id, avatarFile);
|
||||
// Reuse the update method to update the avatar URL.
|
||||
return update(avatarUrl: avatarUrl);
|
||||
} catch (e) {
|
||||
_setResource(_resource.copyWith(isLoading: false, error: toException(e)));
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
58
frontend/pshared/lib/provider/accounts/employees.dart
Normal file
58
frontend/pshared/lib/provider/accounts/employees.dart
Normal file
@@ -0,0 +1,58 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import 'package:pshared/models/organization/employee.dart';
|
||||
import 'package:pshared/provider/organizations.dart';
|
||||
import 'package:pshared/provider/resource.dart';
|
||||
import 'package:pshared/service/accounts/employees.dart';
|
||||
|
||||
|
||||
class EmployeesProvider extends ChangeNotifier {
|
||||
|
||||
Resource<List<Employee>> _employees = Resource<List<Employee>>(data: []);
|
||||
List<Employee> get employees => _employees.data ?? [];
|
||||
bool get isLoading => _employees.isLoading;
|
||||
Object? get error => _employees.error;
|
||||
Employee? getEmployee(String? employeeRef) => employees.firstWhereOrNull((employee) => employee.id == employeeRef);
|
||||
|
||||
|
||||
bool Function(Employee)? _filterPredicate;
|
||||
|
||||
List<Employee> get filteredItems => _filterPredicate != null
|
||||
? employees.where(_filterPredicate!).toList()
|
||||
: employees;
|
||||
|
||||
void setFilterPredicate(bool Function(Employee)? predicate) {
|
||||
_filterPredicate = predicate;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void clearFilter() => setFilterPredicate(null);
|
||||
|
||||
void updateProviders(OrganizationsProvider organizations) {
|
||||
load(organizations.current.id);
|
||||
}
|
||||
|
||||
Future<List<Employee>> load(String organizationRef) async {
|
||||
_employees = _employees.copyWith(isLoading: true, error: null);
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final fetchedEmployees = await EmployeesService.list(organizationRef);
|
||||
_employees = _employees.copyWith(
|
||||
data: fetchedEmployees,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
);
|
||||
} catch (e) {
|
||||
_employees = _employees.copyWith(
|
||||
error: e is Exception ? e : Exception('Unknown error: ${e.toString()}'),
|
||||
isLoading: false,
|
||||
);
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
return employees;
|
||||
}
|
||||
}
|
||||
3
frontend/pshared/lib/provider/exception.dart
Normal file
3
frontend/pshared/lib/provider/exception.dart
Normal file
@@ -0,0 +1,3 @@
|
||||
Exception toException(Object e) {
|
||||
return e is Exception ? e : Exception(e.toString());
|
||||
}
|
||||
29
frontend/pshared/lib/provider/locale.dart
Normal file
29
frontend/pshared/lib/provider/locale.dart
Normal file
@@ -0,0 +1,29 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/config/constants.dart';
|
||||
|
||||
|
||||
class LocaleProvider with ChangeNotifier {
|
||||
Locale _locale = Constants.defaultLocale;
|
||||
|
||||
Locale stringToLocale(String localeString) {
|
||||
var parts = localeString.split(RegExp(r'[-_]'));
|
||||
return (parts.length > 1) ? Locale(parts[0], parts[1]) : Locale(parts[0]);
|
||||
}
|
||||
|
||||
LocaleProvider(String? localeCode) {
|
||||
if (localeCode != null) {
|
||||
_locale = stringToLocale(localeCode);
|
||||
}
|
||||
}
|
||||
|
||||
Locale get locale => _locale;
|
||||
|
||||
void setLocale(Locale locale) {
|
||||
if (_locale == locale) return;
|
||||
|
||||
_locale = locale;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
80
frontend/pshared/lib/provider/organizations.dart
Normal file
80
frontend/pshared/lib/provider/organizations.dart
Normal file
@@ -0,0 +1,80 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import 'package:pshared/config/constants.dart';
|
||||
import 'package:pshared/models/organization/organization.dart';
|
||||
import 'package:pshared/provider/resource.dart';
|
||||
import 'package:pshared/provider/exception.dart';
|
||||
import 'package:pshared/service/organization.dart';
|
||||
import 'package:pshared/service/secure_storage.dart';
|
||||
|
||||
|
||||
class OrganizationsProvider extends ChangeNotifier {
|
||||
Resource<List<Organization>> _resource = Resource(data: []);
|
||||
Resource<List<Organization>> get resource => _resource;
|
||||
|
||||
List<Organization> get organizations => _resource.data ?? [];
|
||||
String? _currentOrg;
|
||||
|
||||
Organization get current => isOrganizationSet ? _current! : throw StateError('Organization is not set');
|
||||
|
||||
Organization? _org(String? orgRef) => organizations.firstWhereOrNull((org) => org.id == orgRef);
|
||||
Organization? get _current => _org(_currentOrg);
|
||||
|
||||
bool get isOrganizationSet => _current != null;
|
||||
bool get isLoading => _resource.isLoading;
|
||||
Object? get error => _resource.error;
|
||||
|
||||
void _setResource(Resource<List<Organization>> newResource) {
|
||||
_resource = newResource;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<List<Organization>> load() async {
|
||||
_setResource(_resource.copyWith(isLoading: true, error: null));
|
||||
try {
|
||||
final orgs = await OrganizationService.list();
|
||||
// fetch stored org
|
||||
String? org = await SecureStorageService.get(Constants.currentOrgKey);
|
||||
// check stored org availability
|
||||
org = orgs.firstWhereOrNull((o) => o.id == org)?.id;
|
||||
// fallback if org is not set or not available
|
||||
org ??= orgs.first.id;
|
||||
await setCurrentOrganization(org);
|
||||
_setResource(Resource(data: orgs, isLoading: false));
|
||||
return orgs;
|
||||
} catch (e) {
|
||||
_setResource(_resource.copyWith(isLoading: false, error: toException(e)));
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Organization> loadByInvitation(String invitationRef) async {
|
||||
_setResource(_resource.copyWith(isLoading: true, error: null));
|
||||
try {
|
||||
final org = await OrganizationService.loadByInvitation(invitationRef);
|
||||
await setCurrentOrganization(org.id);
|
||||
_setResource(Resource(data: [org], isLoading: false));
|
||||
return org;
|
||||
} catch (e) {
|
||||
_setResource(_resource.copyWith(isLoading: false, error: toException(e)));
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
bool _setCurrentOrganization(String? orgRef) {
|
||||
final organizationRef = _org(orgRef)?.id;
|
||||
if (organizationRef == null) return false;
|
||||
|
||||
_currentOrg = organizationRef;
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<bool> setCurrentOrganization(String? orgRef) async {
|
||||
if (!_setCurrentOrganization(orgRef)) return false;
|
||||
await SecureStorageService.set(Constants.currentOrgKey, orgRef);
|
||||
notifyListeners();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
165
frontend/pshared/lib/provider/permissions.dart
Normal file
165
frontend/pshared/lib/provider/permissions.dart
Normal file
@@ -0,0 +1,165 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import 'package:pshared/api/requests/change_role.dart';
|
||||
import 'package:pshared/models/permissions/access.dart';
|
||||
import 'package:pshared/models/permissions/action.dart' as perm;
|
||||
import 'package:pshared/models/permissions/data/permission.dart';
|
||||
import 'package:pshared/models/permissions/data/policy.dart';
|
||||
import 'package:pshared/models/permissions/data/role.dart';
|
||||
import 'package:pshared/models/permissions/descriptions/policy.dart';
|
||||
import 'package:pshared/models/permissions/descriptions/role.dart';
|
||||
import 'package:pshared/models/permissions/effect.dart';
|
||||
import 'package:pshared/models/resources.dart';
|
||||
import 'package:pshared/provider/organizations.dart';
|
||||
import 'package:pshared/provider/resource.dart';
|
||||
import 'package:pshared/service/permissions.dart';
|
||||
|
||||
|
||||
class PermissionsProvider extends ChangeNotifier {
|
||||
Resource<UserAccess> _userAccess = Resource(data: null, isLoading: false, error: null);
|
||||
late OrganizationsProvider _organizations;
|
||||
|
||||
void update(OrganizationsProvider venue) {
|
||||
_organizations = venue;
|
||||
}
|
||||
|
||||
// Generic wrapper to perform service calls and reload state
|
||||
Future<UserAccess?> _performServiceCall(Future Function() operation) async {
|
||||
try {
|
||||
await operation();
|
||||
return await load();
|
||||
} catch (e) {
|
||||
_userAccess = _userAccess.copyWith(
|
||||
error: e is Exception ? e : Exception(e.toString()),
|
||||
isLoading: false,
|
||||
);
|
||||
notifyListeners();
|
||||
return _userAccess.data;
|
||||
}
|
||||
}
|
||||
|
||||
/// Load the [UserAccess] for the current venue.
|
||||
Future<UserAccess?> load() async {
|
||||
_userAccess = _userAccess.copyWith(isLoading: true, error: null);
|
||||
notifyListeners();
|
||||
|
||||
try {
|
||||
final orgRef = _organizations.current.id;
|
||||
final access = await PermissionsService.load(orgRef);
|
||||
_userAccess = _userAccess.copyWith(data: access, isLoading: false);
|
||||
|
||||
if (canRead(ResourceType.roles)) {
|
||||
final allAccess = await PermissionsService.loadAll(orgRef);
|
||||
_userAccess = _userAccess.copyWith(data: allAccess, isLoading: false);
|
||||
}
|
||||
} catch (e) {
|
||||
_userAccess = _userAccess.copyWith(
|
||||
error: e is Exception ? e : Exception(e.toString()),
|
||||
isLoading: false,
|
||||
);
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
return _userAccess.data;
|
||||
}
|
||||
|
||||
Future<UserAccess?> changeRole(String accountRef, String newRoleDescRef) async {
|
||||
final currentRole = roles.firstWhereOrNull((r) => r.accountRef == accountRef);
|
||||
final currentDesc = currentRole != null
|
||||
? roleDescriptions.firstWhereOrNull((d) => d.storable.id == currentRole.descriptionRef)
|
||||
: null;
|
||||
|
||||
if (currentRole == null || currentDesc == null || currentDesc.storable.id == newRoleDescRef) {
|
||||
return _userAccess.data;
|
||||
}
|
||||
return _performServiceCall(() => PermissionsService.changeRole(
|
||||
_organizations.current.id,
|
||||
ChangeRole(accountRef: accountRef, newRoleDescriptionRef: newRoleDescRef),
|
||||
));
|
||||
}
|
||||
|
||||
Future<UserAccess?> deleteRoleDescription(String descRef) {
|
||||
return _performServiceCall(() => PermissionsService.deleteRoleDescription(descRef));
|
||||
}
|
||||
|
||||
Future<UserAccess?> createPermissions(List<Policy> policies) {
|
||||
return _performServiceCall(() => PermissionsService.createPolicies(policies));
|
||||
}
|
||||
|
||||
Future<UserAccess?> deletePermissions(List<Policy> policies) {
|
||||
return _performServiceCall(() => PermissionsService.deletePolicies(policies));
|
||||
}
|
||||
|
||||
Future<UserAccess?> changePermissions(List<Policy> add, List<Policy> remove) {
|
||||
return _performServiceCall(() => PermissionsService.changePolicies(add, remove));
|
||||
}
|
||||
|
||||
// -- Data getters --
|
||||
Set<ResourceType> extractResourceTypes(Iterable<PolicyDescription> descriptions) => descriptions.expand((policy) => policy.resourceTypes ?? <ResourceType>[]).toSet();
|
||||
|
||||
Set<ResourceType> get resources => Set.unmodifiable(extractResourceTypes(policyDescriptions));
|
||||
|
||||
Set<ResourceType> getRoleResources(String roleDescRef) => Set.unmodifiable(
|
||||
extractResourceTypes(
|
||||
getRolePermissions(roleDescRef)
|
||||
.map((p) => getPolicyDescription(p.policy.descriptionRef))
|
||||
.whereType<PolicyDescription>(),
|
||||
),
|
||||
);
|
||||
|
||||
String? getPolicyDescriptionRef(ResourceType resource) => policyDescriptions.firstWhereOrNull((p) => p.resourceTypes?.contains(resource) ?? false)?.storable.id;
|
||||
|
||||
List<PolicyDescription> get policyDescriptions => List.unmodifiable(_userAccess.data?.descriptions.policies ?? []);
|
||||
List<RoleDescription> get roleDescriptions => List.unmodifiable(_userAccess.data?.descriptions.roles ?? []);
|
||||
List<Permission> get permissions => List.unmodifiable(_userAccess.data?.permissions.permissions ?? []);
|
||||
List<Policy> get policies => List.unmodifiable(_userAccess.data?.permissions.policies ?? []);
|
||||
List<Role> get roles => List.unmodifiable(_userAccess.data?.permissions.roles ?? []);
|
||||
|
||||
Role? getRole(String accountRef) => roles.firstWhereOrNull((r) => r.accountRef == accountRef);
|
||||
RoleDescription? getRoleDescription(String descRef) => roleDescriptions.firstWhereOrNull((d) => d.storable.id == descRef);
|
||||
List<Role> getRoles(String accountRef) => roles.where((r) => r.accountRef == accountRef).toList();
|
||||
List<Policy> getRolePolicies(String roleRef) => policies.where((p) => p.roleDescriptionRef == roleRef).toList();
|
||||
List<Permission> getRolePermissions(String descRef) => permissions.where((p) => p.policy.roleDescriptionRef == descRef).toList();
|
||||
PolicyDescription? getPolicyDescription(String policyRef) => policyDescriptions.firstWhereOrNull((p) => p.storable.id == policyRef);
|
||||
|
||||
// -- Permission checks --
|
||||
bool get isLoading => _userAccess.isLoading;
|
||||
bool get isReady => !_userAccess.isLoading && error == null;
|
||||
Exception? get error => _userAccess.error;
|
||||
|
||||
bool _hasMatchingPermission(
|
||||
PolicyDescription pd,
|
||||
Effect effect,
|
||||
perm.Action? action, {
|
||||
Object? objectRef,
|
||||
}) => permissions.firstWhereOrNull(
|
||||
(p) =>
|
||||
p.policy.descriptionRef == pd.storable.id &&
|
||||
p.policy.effect.effect == effect &&
|
||||
(action == null || p.policy.effect.action == action) &&
|
||||
(p.policy.objectRef == null || p.policy.objectRef == objectRef),
|
||||
) != null;
|
||||
|
||||
bool canAccessResource(
|
||||
ResourceType resource, {
|
||||
perm.Action? action,
|
||||
Object? objectRef,
|
||||
}) {
|
||||
final orgId = _organizations.current.id;
|
||||
final pd = policyDescriptions.firstWhereOrNull(
|
||||
(policy) =>
|
||||
(policy.resourceTypes?.contains(resource) ?? false) &&
|
||||
(policy.organizationRef == null || policy.organizationRef == orgId),
|
||||
);
|
||||
if (pd == null) return false;
|
||||
if (_hasMatchingPermission(pd, Effect.deny, action, objectRef: objectRef)) return false;
|
||||
return _hasMatchingPermission(pd, Effect.allow, action, objectRef: objectRef);
|
||||
}
|
||||
|
||||
bool canRead(ResourceType r, {Object? objectRef}) => canAccessResource(r, action: perm.Action.read, objectRef: objectRef);
|
||||
bool canUpdate(ResourceType r, {Object? objectRef}) => canAccessResource(r, action: perm.Action.update, objectRef: objectRef);
|
||||
bool canDelete(ResourceType r, {Object? objectRef}) => canAccessResource(r, action: perm.Action.delete, objectRef: objectRef);
|
||||
bool canCreate(ResourceType r) => canAccessResource(r, action: perm.Action.create);
|
||||
}
|
||||
51
frontend/pshared/lib/provider/pfe/provider.dart
Normal file
51
frontend/pshared/lib/provider/pfe/provider.dart
Normal file
@@ -0,0 +1,51 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:pshared/service/pfe/service.dart';
|
||||
import 'package:pshared/provider/exception.dart';
|
||||
import 'package:pshared/provider/resource.dart';
|
||||
import 'package:pshared/service/account.dart';
|
||||
|
||||
|
||||
class PfeProvider extends ChangeNotifier {
|
||||
// The resource now wraps our Account? state along with its loading/error state.
|
||||
Resource<String?> _resource = Resource(data: null);
|
||||
Resource<String?> get resource => _resource;
|
||||
|
||||
String? get session => _resource.data;
|
||||
bool get isLoggedIn => session != null;
|
||||
bool get isLoading => _resource.isLoading;
|
||||
Object? get error => _resource.error;
|
||||
|
||||
// Private helper to update the resource and notify listeners.
|
||||
void _setResource(Resource<String?> newResource) {
|
||||
_resource = newResource;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
|
||||
Future<String?> login({
|
||||
required String email,
|
||||
required String password,
|
||||
}) async {
|
||||
_setResource(_resource.copyWith(isLoading: true, error: null));
|
||||
try {
|
||||
final acc = await PfeService.login(email, password);
|
||||
_setResource(Resource(data: acc, isLoading: false));
|
||||
return acc;
|
||||
} catch (e) {
|
||||
_setResource(_resource.copyWith(isLoading: false, error: toException(e)));
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
_setResource(_resource.copyWith(isLoading: true, error: null));
|
||||
try {
|
||||
await AccountService.logout();
|
||||
_setResource(Resource(data: null, isLoading: false));
|
||||
} catch (e) {
|
||||
_setResource(_resource.copyWith(isLoading: false, error: toException(e)));
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
15
frontend/pshared/lib/provider/resource.dart
Normal file
15
frontend/pshared/lib/provider/resource.dart
Normal file
@@ -0,0 +1,15 @@
|
||||
class Resource<T> {
|
||||
final T? data;
|
||||
final bool isLoading;
|
||||
final Exception? error;
|
||||
|
||||
Resource({this.data, this.isLoading = false, this.error});
|
||||
|
||||
Resource<T> copyWith({T? data, bool? isLoading, Exception? error}) {
|
||||
return Resource<T>(
|
||||
data: data ?? this.data,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
error: error ?? this.error,
|
||||
);
|
||||
}
|
||||
}
|
||||
0
frontend/pshared/lib/provider/services.dart
Normal file
0
frontend/pshared/lib/provider/services.dart
Normal file
170
frontend/pshared/lib/provider/template.dart
Normal file
170
frontend/pshared/lib/provider/template.dart
Normal file
@@ -0,0 +1,170 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
|
||||
import 'package:pshared/models/permission_bound_storable.dart';
|
||||
import 'package:pshared/provider/exception.dart';
|
||||
import 'package:pshared/provider/resource.dart';
|
||||
import 'package:pshared/service/template.dart';
|
||||
|
||||
|
||||
List<T> mergeLists<T>({
|
||||
required List<T> lhs,
|
||||
required List<T> rhs,
|
||||
required Comparable Function(T) getKey, // Extracts ID dynamically
|
||||
required int Function(T, T) compare,
|
||||
required T Function(T, T) merge,
|
||||
}) {
|
||||
final result = <T>[];
|
||||
final map = {for (var item in lhs) getKey(item): item};
|
||||
|
||||
for (var updated in rhs) {
|
||||
final key = getKey(updated);
|
||||
map[key] = merge(map[key] ?? updated, updated);
|
||||
}
|
||||
|
||||
result.addAll(map.values);
|
||||
result.sort(compare);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// A generic provider that wraps a [BasicService] instance
|
||||
/// to manage state (loading, error, data) without re‑implementing service logic.
|
||||
class GenericProvider<T extends PermissionBoundStorable> extends ChangeNotifier {
|
||||
final BasicService<T> service;
|
||||
|
||||
Resource<List<T>> _resource = Resource(data: []);
|
||||
Resource<List<T>> get resource => _resource;
|
||||
|
||||
List<T> get items => List.unmodifiable(_resource.data ?? []);
|
||||
bool get isLoading => _resource.isLoading;
|
||||
bool get isEmpty => items.isEmpty;
|
||||
Object? get error => _resource.error;
|
||||
|
||||
String? _currentObjectRef; // Stores the currently selected project ref
|
||||
T? get currentObject => _resource.data?.firstWhereOrNull(
|
||||
(object) => object.id == _currentObjectRef,
|
||||
);
|
||||
|
||||
T? getItemById(String id) => items.firstWhereOrNull((item) => item.id == id);
|
||||
|
||||
GenericProvider({required this.service});
|
||||
|
||||
|
||||
bool Function(T)? _filterPredicate;
|
||||
|
||||
List<T> get filteredItems => _filterPredicate != null ? items.where(_filterPredicate!).toList() : items;
|
||||
|
||||
void setFilterPredicate(bool Function(T)? predicate) {
|
||||
_filterPredicate = predicate;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void clearFilter() => setFilterPredicate(null);
|
||||
|
||||
void _setResource(Resource<List<T>> newResource) {
|
||||
_resource = newResource;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> loadFuture(Future<List<T>> future) async {
|
||||
_setResource(_resource.copyWith(isLoading: true));
|
||||
try {
|
||||
final list = await future;
|
||||
_setResource(Resource(data: list, isLoading: false));
|
||||
} catch (e) {
|
||||
_setResource(
|
||||
_resource.copyWith(isLoading: false, error: toException(e)),
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> load(String organizationRef, String? parentRef) async {
|
||||
if (parentRef != null) {
|
||||
return loadFuture(service.list(organizationRef, parentRef));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadItem(String itemRef) async {
|
||||
return loadFuture((() async => [await service.get(itemRef)])());
|
||||
}
|
||||
|
||||
|
||||
List<T> merge(List<T> rhs) => mergeLists<T>(
|
||||
lhs: items,
|
||||
rhs: rhs,
|
||||
getKey: (item) => item.id, // Key extractor
|
||||
compare: (a, b) => a.id.compareTo(b.id), // Sorting logic
|
||||
merge: (existing, updated) => updated, // Replace with the updated version
|
||||
);
|
||||
|
||||
Future<T> get(String objectRef) async {
|
||||
_setResource(_resource.copyWith(isLoading: true));
|
||||
try {
|
||||
final item = await service.get(objectRef);
|
||||
_setResource(Resource(data: merge([item]), isLoading: false));
|
||||
return item;
|
||||
} catch (e) {
|
||||
_setResource(_resource.copyWith(isLoading: false, error: toException(e)));
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<T> createObject(String organizationRef, Map<String, dynamic> request) async {
|
||||
_setResource(_resource.copyWith(isLoading: true));
|
||||
try {
|
||||
final newObject = await service.create(organizationRef, request);
|
||||
_setResource(Resource(data: [...items, ...newObject], isLoading: false));
|
||||
return newObject.first;
|
||||
} catch (e) {
|
||||
_setResource(_resource.copyWith(isLoading: false, error: toException(e)));
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> update(Map<String, dynamic> request) async {
|
||||
_setResource(_resource.copyWith(isLoading: true));
|
||||
try {
|
||||
final list = await service.update(request);
|
||||
_setResource(Resource(data: merge(list), isLoading: false));
|
||||
} catch (e) {
|
||||
_setResource(_resource.copyWith(isLoading: false, error: toException(e)));
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> delete(String objectRef) async {
|
||||
_setResource(_resource.copyWith(isLoading: true));
|
||||
|
||||
try {
|
||||
await service.delete(objectRef);
|
||||
if (_currentObjectRef == objectRef) {
|
||||
_currentObjectRef = null;
|
||||
}
|
||||
|
||||
_setResource(Resource(
|
||||
data: _resource.data?.where((p) => p.id != objectRef).toList(),
|
||||
isLoading: false,
|
||||
));
|
||||
} catch (e) {
|
||||
_setResource(Resource(data: _resource.data, isLoading: false, error: toException(e)));
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
bool setCurrentObject(String? objectRef) {
|
||||
if (objectRef == null) {
|
||||
_currentObjectRef = null;
|
||||
notifyListeners();
|
||||
return true;
|
||||
}
|
||||
if (_resource.data?.any((p) => p.id == objectRef) ?? false) {
|
||||
_currentObjectRef = objectRef;
|
||||
notifyListeners();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false; // Object not found
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user