Frontend first draft

This commit is contained in:
Arseni
2025-11-13 15:06:15 +03:00
parent e47f343afb
commit ddb54ddfdc
504 changed files with 25498 additions and 1 deletions

View File

@@ -0,0 +1,36 @@
import 'package:pshared/models/account/base.dart';
class Account extends AccountBase {
final String login;
const Account({
required super.storable,
required super.avatarUrl,
required this.login,
required super.locale,
required super.name,
});
factory Account.fromBase(AccountBase accountBase, String login) => Account(
storable: accountBase.storable,
avatarUrl: accountBase.avatarUrl,
locale: accountBase.locale,
name: accountBase.name,
login: login,
);
@override
Account copyWith({
String? Function()? avatarUrl,
String? name,
String? locale,
}) {
final updatedBase = super.copyWith(
avatarUrl: avatarUrl,
name: name,
locale: locale,
);
return Account.fromBase(updatedBase, login);
}
}

View File

@@ -0,0 +1,35 @@
import 'package:pshared/models/storable.dart';
class AccountBase implements Storable {
final Storable storable;
@override
String get id => storable.id;
@override
DateTime get createdAt => storable.createdAt;
@override
DateTime get updatedAt => storable.updatedAt;
final String? avatarUrl;
final String name;
final String locale;
const AccountBase({
required this.storable,
required this.name,
required this.locale,
required this.avatarUrl,
});
AccountBase copyWith({
String? Function()? avatarUrl,
String? name,
String? locale,
}) => AccountBase(
storable: storable,
avatarUrl: avatarUrl != null ? avatarUrl() : this.avatarUrl,
locale: locale ?? this.locale,
name: name ?? this.name,
);
}

View File

@@ -0,0 +1,7 @@
class OrganizationDescription {
final String? logoUrl;
const OrganizationDescription({
this.logoUrl,
});
}

View File

@@ -0,0 +1,4 @@
import 'package:pshared/models/account/account.dart';
typedef Employee = Account;

View File

@@ -0,0 +1,34 @@
import 'package:pshared/models/storable.dart';
class Organization implements Storable {
final Storable storable;
@override
String get id => storable.id;
@override
DateTime get createdAt => storable.createdAt;
@override
DateTime get updatedAt => storable.updatedAt;
final String timeZone;
final String? logoUrl;
const Organization({
required this.storable,
required this.timeZone,
this.logoUrl,
});
Organization copyWith({
String? name,
String? Function()? description,
String? timeZone,
String? Function()? logoUrl,
}) => Organization(
storable: storable, // Same Storable, same id
timeZone: timeZone ?? this.timeZone,
logoUrl: logoUrl != null ? logoUrl() : this.logoUrl,
);
}

View File

@@ -0,0 +1,18 @@
import 'package:pshared/models/payment/methods/data.dart';
import 'package:pshared/models/payment/type.dart';
class CardPaymentMethod extends PaymentMethodData {
@override
final PaymentType type = PaymentType.card;
final String pan;
final String firstName;
final String lastName;
CardPaymentMethod({
required this.pan,
required this.firstName,
required this.lastName,
});
}

View File

@@ -0,0 +1,6 @@
import 'package:pshared/models/payment/type.dart';
abstract class PaymentMethodData {
PaymentType get type;
}

View File

@@ -0,0 +1,20 @@
import 'package:pshared/models/payment/methods/data.dart';
import 'package:pshared/models/payment/type.dart';
class IbanPaymentMethod extends PaymentMethodData {
@override
final PaymentType type = PaymentType.iban;
final String iban; // e.g. DE89 3704 0044 0532 0130 00
final String accountHolder; // Full name of the recipient
final String? bic; // Optional: for cross-border transfers
final String? bankName; // Optional: for UI clarity
IbanPaymentMethod({
required this.iban,
required this.accountHolder,
this.bic,
this.bankName,
});
}

View File

@@ -0,0 +1,26 @@
import 'package:pshared/models/payment/methods/data.dart';
import 'package:pshared/models/payment/type.dart';
class RussianBankAccountPaymentMethod extends PaymentMethodData {
@override
final PaymentType type = PaymentType.bankAccount;
final String recipientName;
final String inn;
final String kpp;
final String bankName;
final String bik;
final String accountNumber;
final String correspondentAccount;
RussianBankAccountPaymentMethod({
required this.recipientName,
required this.inn,
required this.kpp,
required this.bankName,
required this.bik,
required this.accountNumber,
required this.correspondentAccount,
});
}

View File

@@ -0,0 +1,21 @@
import 'package:pshared/models/payment/type.dart';
class PaymentMethod {
PaymentMethod({
required this.id,
required this.label,
required this.details,
required this.type,
this.isEnabled = true,
this.isMain = false,
});
final String id;
final String label;
final String details;
final PaymentType type;
bool isEnabled;
bool isMain;
}

View File

@@ -0,0 +1,12 @@
import 'package:pshared/models/payment/methods/data.dart';
import 'package:pshared/models/payment/type.dart';
class WalletPaymentMethod extends PaymentMethodData {
@override
final PaymentType type = PaymentType.wallet;
final String walletId;
WalletPaymentMethod({required this.walletId});
}

View File

@@ -0,0 +1,30 @@
import 'package:pshared/models/payment/status.dart';
class OperationItem {
final OperationStatus status;
final String? fileName;
final double amount;
final String currency;
final double toAmount;
final String toCurrency;
final String payId;
final String? cardNumber;
final String name;
final DateTime date;
final String comment;
OperationItem({
required this.status,
this.fileName,
required this.amount,
required this.currency,
required this.toAmount,
required this.toCurrency,
required this.payId,
this.cardNumber,
required this.name,
required this.date,
required this.comment,
});
}

View File

@@ -0,0 +1,25 @@
import 'package:flutter/widgets.dart';
import 'package:pshared/generated/i18n/ps_localizations.dart';
enum OperationStatus {
processing,
success,
error,
}
extension OperationStatusX on OperationStatus {
/// Returns the localized string for this status,
/// e.g. “Processing”, “Success”, “Error”.
String localized(BuildContext context) {
final loc = PSLocalizations.of(context)!;
switch (this) {
case OperationStatus.processing:
return loc.operationStatusProcessing;
case OperationStatus.success:
return loc.operationStatusSuccess;
case OperationStatus.error:
return loc.operationStatusError;
}
}
}

View File

@@ -0,0 +1,6 @@
enum PaymentType {
bankAccount,
iban,
wallet,
card,
}

View File

@@ -0,0 +1,11 @@
class UploadHistoryItem {
final String name;
final String status;
final String time;
UploadHistoryItem({
required this.name,
required this.status,
required this.time,
});
}

View File

@@ -0,0 +1,22 @@
import 'package:pshared/config/constants.dart';
abstract class PermissionBound {
String get permissionRef;
String get organizationRef;
}
class _PermissionBoundImp implements PermissionBound {
@override
final String permissionRef;
@override
final String organizationRef;
const _PermissionBoundImp({
required this.permissionRef,
required this.organizationRef,
});
}
PermissionBound newPermissionBound({ required String organizationRef, String? permissionRef}) =>
_PermissionBoundImp(permissionRef: permissionRef ?? Constants.nilObjectRef, organizationRef: organizationRef);

View File

@@ -0,0 +1,6 @@
import 'package:pshared/models/permission_bound.dart';
import 'package:pshared/models/storable.dart';
abstract class PermissionBoundStorable implements PermissionBound, Storable {
}

View File

@@ -0,0 +1,13 @@
import 'package:pshared/models/permissions/data/permissions.dart';
import 'package:pshared/models/permissions/descriptions/permissions.dart';
class UserAccess {
final PermissionsDescription descriptions;
final PermissionsData permissions;
const UserAccess({
required this.descriptions,
required this.permissions,
});
}

View File

@@ -0,0 +1,15 @@
enum Action {
create,
read,
update,
delete,
}
extension ActionExtension on Action {
String toShortString() => toString().split('.').last;
static Action fromString(String value) => Action.values.firstWhere(
(e) => e.toShortString() == value,
orElse: () => throw ArgumentError('Invalid action: $value'),
);
}

View File

@@ -0,0 +1,13 @@
import 'package:pshared/models/permissions/action.dart';
import 'package:pshared/models/permissions/effect.dart';
class ActionEffect {
final Action action; // The action allowed or denied
final Effect effect; // The effect of the policy ("allow" or "deny")
const ActionEffect({
required this.action,
required this.effect,
});
}

View File

@@ -0,0 +1,12 @@
import 'package:pshared/models/permissions/data/policy.dart';
class Permission {
final Policy policy;
final String accountRef;
const Permission({
required this.policy,
required this.accountRef,
});
}

View File

@@ -0,0 +1,16 @@
import 'package:pshared/models/permissions/data/permission.dart';
import 'package:pshared/models/permissions/data/policy.dart';
import 'package:pshared/models/permissions/data/role.dart';
class PermissionsData {
final List<Role> roles;
final List<Policy> policies;
final List<Permission> permissions;
const PermissionsData({
required this.roles,
required this.policies,
required this.permissions,
});
}

View File

@@ -0,0 +1,18 @@
import 'package:pshared/models/permissions/action_effect.dart';
class Policy {
final String roleDescriptionRef;
final String organizationRef;
final String descriptionRef;
final String? objectRef;
final ActionEffect effect;
const Policy({
required this.roleDescriptionRef,
required this.organizationRef,
required this.descriptionRef,
required this.objectRef,
required this.effect,
});
}

View File

@@ -0,0 +1,11 @@
class Role {
final String accountRef;
final String organizationRef;
final String descriptionRef;
const Role({
required this.accountRef,
required this.descriptionRef,
required this.organizationRef,
});
}

View File

@@ -0,0 +1,13 @@
import 'package:pshared/models/permissions/descriptions/policy.dart';
import 'package:pshared/models/permissions/descriptions/role.dart';
class PermissionsDescription {
final List<RoleDescription> roles;
final List<PolicyDescription> policies;
const PermissionsDescription({
required this.roles,
required this.policies,
});
}

View File

@@ -0,0 +1,22 @@
import 'package:pshared/models/resources.dart';
import 'package:pshared/models/storable.dart';
class PolicyDescription implements Storable {
final Storable storable;
final List<ResourceType>? resourceTypes;
final String? organizationRef;
@override
String get id => storable.id;
@override
DateTime get createdAt => storable.createdAt;
@override
DateTime get updatedAt => storable.updatedAt;
const PolicyDescription({
required this.storable,
required this.resourceTypes,
required this.organizationRef,
});
}

View File

@@ -0,0 +1,27 @@
import 'package:pshared/models/storable.dart';
class RoleDescription implements Storable {
final Storable storable;
@override
String get id => storable.id;
@override
DateTime get createdAt => storable.createdAt;
@override
DateTime get updatedAt => storable.updatedAt;
final String organizationRef;
const RoleDescription({
required this.storable,
required this.organizationRef,
});
factory RoleDescription.build({
required String organizationRef,
}) => RoleDescription(
storable: newStorable(),
organizationRef: organizationRef
);
}

View File

@@ -0,0 +1,13 @@
enum Effect {
allow,
deny,
}
extension EffectExtension on Effect {
String toShortString() => toString().split('.').last;
static Effect fromString(String value) => Effect.values.firstWhere(
(e) => e.toShortString() == value,
orElse: () => throw ArgumentError('Invalid effect: $value'),
);
}

View File

@@ -0,0 +1 @@
enum RecipientFilter { all, ready, registered, notRegistered }

View File

@@ -0,0 +1,78 @@
import 'package:pshared/models/payment/methods/card.dart';
import 'package:pshared/models/payment/methods/iban.dart';
import 'package:pshared/models/payment/methods/russian_bank.dart';
import 'package:pshared/models/payment/methods/wallet.dart';
import 'package:pshared/models/recipient/status.dart';
import 'package:pshared/models/recipient/type.dart';
class Recipient {
final String? avatarUrl; // network URL / local asset
final String name;
final String email;
final RecipientStatus status;
final RecipientType type;
final CardPaymentMethod? card;
final IbanPaymentMethod? iban;
final RussianBankAccountPaymentMethod? bank;
final WalletPaymentMethod? wallet;
const Recipient({
this.avatarUrl,
required this.name,
required this.email,
required this.status,
required this.type,
this.card,
this.iban,
this.bank,
this.wallet,
});
/// Convenience factory for quickly creating mock recipients.
factory Recipient.mock({
required String name,
required String email,
required RecipientStatus status,
required RecipientType type,
CardPaymentMethod? card,
IbanPaymentMethod? iban,
RussianBankAccountPaymentMethod? bank,
WalletPaymentMethod? wallet,
}) =>
Recipient(
avatarUrl: null,
name: name,
email: email,
status: status,
type: type,
card: card,
iban: iban,
bank: bank,
wallet: wallet,
);
bool matchesQuery(String q) {
final searchable = [
name,
email,
card?.pan,
card?.firstName,
card?.lastName,
iban?.iban,
iban?.accountHolder,
iban?.bic,
iban?.bankName,
bank?.accountNumber,
bank?.recipientName,
bank?.inn,
bank?.kpp,
bank?.bankName,
bank?.bik,
bank?.correspondentAccount,
wallet?.walletId,
];
return searchable.any((field) => field?.toLowerCase().contains(q) ?? false);
}
}

View File

@@ -0,0 +1,22 @@
import 'package:flutter/widgets.dart';
import 'package:pshared/generated/i18n/ps_localizations.dart';
/// Possible payout readiness states.
enum RecipientStatus { ready, registered, notRegistered }
extension RecipientStatusExtension on RecipientStatus {
/// Human-readable, **localized** label for display in the UI.
String label(BuildContext context) {
final l10n = PSLocalizations.of(context)!;
switch (this) {
case RecipientStatus.ready:
return l10n.statusReady;
case RecipientStatus.registered:
return l10n.statusRegistered;
case RecipientStatus.notRegistered:
return l10n.statusNotRegistered;
}
}
}

View File

@@ -0,0 +1,15 @@
import 'package:flutter/widgets.dart';
import 'package:pshared/generated/i18n/ps_localizations.dart';
/// Indicates whether you (internal) or the other party (external) manage payout data.
enum RecipientType { internal, external }
extension RecipientTypeExtension on RecipientType {
/// Localized label no opaque abbreviations.
String label(BuildContext context) =>
this == RecipientType.internal
? PSLocalizations.of(context)!.typeInternal
: PSLocalizations.of(context)!.typeExternal;
}

View File

@@ -0,0 +1,107 @@
import 'package:json_annotation/json_annotation.dart';
/// Represents various resource types (mirroring your Go "Type" constants).
enum ResourceType {
/// Represents user accounts in the system
@JsonValue('accounts')
accounts,
/// Represents analytics integration with Amplitude
@JsonValue('amplitude')
amplitude,
/// Represents automation workflows
@JsonValue('automations')
automations,
/// Tracks changes made to resources
@JsonValue('changes')
changes,
/// Represents client information
@JsonValue('clients')
clients,
/// Represents comments on tasks or other resources
@JsonValue('comments')
comments,
/// Represents invitations sent to users
@JsonValue('invitations')
invitations,
/// Represents invoices
@JsonValue('invoices')
invoices,
/// Represents logos for organizations or projects
@JsonValue('logo')
logo,
/// Represents notifications sent to users
@JsonValue('notifications')
notifications,
/// Represents organizations in the system
@JsonValue('organizations')
organizations,
/// Represents permissions service
@JsonValue('permissions')
permissions,
/// Represents access control policies
@JsonValue('policies')
policies,
/// Represents task or project priorities
@JsonValue('priorities')
priorities,
/// Represents priority groups
@JsonValue('priority_groups')
priorityGroups,
/// Represents projects managed in the system
@JsonValue('projects')
projects,
@JsonValue('properties')
properties,
/// Represents reactions
@JsonValue('reactions')
reactions,
/// Represents refresh tokens for authentication
@JsonValue('refresh_tokens')
refreshTokens,
/// Represents roles in access control
@JsonValue('roles')
roles,
/// Represents statuses of tasks or projects
@JsonValue('statuses')
statuses,
/// Represents steps in workflows or processes
@JsonValue('steps')
steps,
/// Represents tasks managed in the system
@JsonValue('tasks')
tasks,
/// Represents teams managed in the system
@JsonValue('teams')
teams,
/// Represents workflows for tasks or projects
@JsonValue('workflows')
workflows,
/// Represents workspaces containing projects and teams
@JsonValue('workspaces')
workspaces;
}

View File

@@ -0,0 +1,77 @@
import 'package:flutter/material.dart';
typedef LangLocalization = Map<String, String>;
String _translation(LangLocalization loc, String key) {
return loc[key] ?? '';
}
typedef Localizations = Map<String, LangLocalization>;
typedef Localizer = String Function(BuildContext);
class Localization {
static const String keyHint = 'hint';
static const String keyLink = 'link';
static const String keyName = 'name';
static const String keyError = 'error';
static const String keyAddress = 'address';
static const String keyDetails = 'details';
static const String keyRoute = 'route';
static const String keyLocationName = 'location_name';
static String _localizeImp(Localizations localizations, String locale, String Function(LangLocalization) functor) {
final localization = localizations[locale];
if (localization != null) {
return functor(localization);
}
return '';
}
static String _localize(Localizations localizations, String locale, String Function(LangLocalization) functor, {String? fallback}) {
final res = _localizeImp(localizations, locale, functor);
return res.isNotEmpty ? res : (fallback ?? '');
}
static bool localizationExists(Localizations loc, String locale) {
return loc.containsKey(locale);
}
static String hint(Localizations loc, String locale, {String? fallback}) {
return _localize(loc, locale, (localization) => _translation(localization, keyHint), fallback: fallback);
}
static String link(Localizations loc, String locale, {String? fallback}) {
return _localize(loc, locale, (localization) => _translation(localization, keyLink), fallback: fallback);
}
static String name(Localizations loc, String locale, {String? fallback}) {
return _localize(loc, locale, (localization) => _translation(localization, keyName), fallback: fallback);
}
static String error(Localizations loc, String locale, {String? fallback}) {
return _localize(loc, locale, (localization) => _translation(localization, keyError), fallback: fallback);
}
static String address(Localizations loc, String locale, {String? fallback}) {
return _localize(loc, locale, (localization) => _translation(localization, keyAddress), fallback: fallback);
}
static String details(Localizations loc, String locale, {String? fallback}) {
return _localize(loc, locale, (localization) => _translation(localization, keyDetails), fallback: fallback);
}
static String route(Localizations loc, String locale, {String? fallback}) {
return _localize(loc, locale, (localization) => _translation(localization, keyRoute), fallback: fallback);
}
static String locationName(Localizations loc, String locale, {String? fallback}) {
return _localize(loc, locale, (localization) => _translation(localization, keyLocationName), fallback: fallback);
}
static String translate(Localizations loc, String locale, String key, {String? fallback}) {
return _localize(loc, locale, (localization) => _translation(localization, key), fallback: fallback);
}
}

View File

@@ -0,0 +1,27 @@
import 'package:json_annotation/json_annotation.dart';
part 'time_validity.g.dart';
@JsonSerializable()
class TimeValidity {
final DateTime? start;
final DateTime? expiry;
const TimeValidity({this.start, this.expiry});
bool get isExpired => expiry?.isBefore(DateTime.now()) ?? false;
bool get isNotStarted => start?.isAfter(DateTime.now()) ?? false;
bool get isActive => (!isNotStarted) && (!isExpired);
TimeValidity copyWith({
DateTime? Function()? start,
DateTime? Function()? expiry,
}) => TimeValidity(
start: start == null ? this.start : start(),
expiry: expiry == null ? this.expiry : expiry(),
);
factory TimeValidity.fromJson(Map<String, dynamic> json) => _$TimeValidityFromJson(json);
Map<String, dynamic> toJson() => _$TimeValidityToJson(this);
}

View File

@@ -0,0 +1,30 @@
import 'package:pshared/config/constants.dart';
abstract class Storable {
String get id;
DateTime get createdAt;
DateTime get updatedAt;
}
class _StorableImp implements Storable {
@override
final String id;
@override
final DateTime createdAt;
@override
final DateTime updatedAt;
const _StorableImp({
required this.id,
required this.createdAt,
required this.updatedAt,
});
}
Storable newStorable({String? id, DateTime? createdAt, DateTime? updatedAt}) => _StorableImp(
id: id ?? Constants.nilObjectRef,
createdAt: createdAt ?? DateTime.now().toUtc(),
updatedAt: updatedAt ?? DateTime.now().toUtc(),
);