accounts creation
All checks were successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/discovery Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/gateway_mntx Pipeline was successful
ci/woodpecker/push/gateway_chain Pipeline was successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
All checks were successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/discovery Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/gateway_mntx Pipeline was successful
ci/woodpecker/push/gateway_chain Pipeline was successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
This commit is contained in:
@@ -7,12 +7,13 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/tech/sendico/pkg/ledgerconv"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
"github.com/tech/sendico/pkg/payments/rail"
|
"github.com/tech/sendico/pkg/payments/rail"
|
||||||
describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1"
|
describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1"
|
||||||
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
|
||||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
|
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
|
||||||
|
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
|
||||||
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/credentials"
|
"google.golang.org/grpc/credentials"
|
||||||
@@ -192,7 +193,6 @@ func (c *ledgerClient) CreateAccount(ctx context.Context, req *ledgerv1.CreateAc
|
|||||||
}
|
}
|
||||||
params := map[string]interface{}{
|
params := map[string]interface{}{
|
||||||
"organization_ref": strings.TrimSpace(req.GetOrganizationRef()),
|
"organization_ref": strings.TrimSpace(req.GetOrganizationRef()),
|
||||||
"account_code": strings.TrimSpace(req.GetAccountCode()),
|
|
||||||
"account_type": req.GetAccountType().String(),
|
"account_type": req.GetAccountType().String(),
|
||||||
"status": req.GetStatus().String(),
|
"status": req.GetStatus().String(),
|
||||||
"allow_negative": req.GetAllowNegative(),
|
"allow_negative": req.GetAllowNegative(),
|
||||||
@@ -523,29 +523,13 @@ func ledgerAccountFromConnector(account *connectorv1.Account) *ledgerv1.LedgerAc
|
|||||||
}
|
}
|
||||||
|
|
||||||
func parseAccountType(value string) ledgerv1.AccountType {
|
func parseAccountType(value string) ledgerv1.AccountType {
|
||||||
switch strings.ToUpper(strings.TrimSpace(value)) {
|
accountType, _ := ledgerconv.ParseAccountType(value)
|
||||||
case "ACCOUNT_TYPE_ASSET", "ASSET":
|
return accountType
|
||||||
return ledgerv1.AccountType_ACCOUNT_TYPE_ASSET
|
|
||||||
case "ACCOUNT_TYPE_LIABILITY", "LIABILITY":
|
|
||||||
return ledgerv1.AccountType_ACCOUNT_TYPE_LIABILITY
|
|
||||||
case "ACCOUNT_TYPE_REVENUE", "REVENUE":
|
|
||||||
return ledgerv1.AccountType_ACCOUNT_TYPE_REVENUE
|
|
||||||
case "ACCOUNT_TYPE_EXPENSE", "EXPENSE":
|
|
||||||
return ledgerv1.AccountType_ACCOUNT_TYPE_EXPENSE
|
|
||||||
default:
|
|
||||||
return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseAccountStatus(value string) ledgerv1.AccountStatus {
|
func parseAccountStatus(value string) ledgerv1.AccountStatus {
|
||||||
switch strings.ToUpper(strings.TrimSpace(value)) {
|
status, _ := ledgerconv.ParseAccountStatus(value)
|
||||||
case "ACCOUNT_STATUS_ACTIVE", "ACTIVE":
|
return status
|
||||||
return ledgerv1.AccountStatus_ACCOUNT_STATUS_ACTIVE
|
|
||||||
case "ACCOUNT_STATUS_FROZEN", "FROZEN":
|
|
||||||
return ledgerv1.AccountStatus_ACCOUNT_STATUS_FROZEN
|
|
||||||
default:
|
|
||||||
return ledgerv1.AccountStatus_ACCOUNT_STATUS_UNSPECIFIED
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func journalEntryFromOperation(op *connectorv1.Operation) *ledgerv1.JournalEntryResponse {
|
func journalEntryFromOperation(op *connectorv1.Operation) *ledgerv1.JournalEntryResponse {
|
||||||
|
|||||||
@@ -37,12 +37,6 @@ func (s *Service) createAccountResponder(_ context.Context, req *ledgerv1.Create
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
accountCode := strings.TrimSpace(req.GetAccountCode())
|
|
||||||
if accountCode == "" {
|
|
||||||
return nil, merrors.InvalidArgument("account_code is required")
|
|
||||||
}
|
|
||||||
accountCode = strings.ToLower(accountCode)
|
|
||||||
|
|
||||||
currency := strings.TrimSpace(req.GetCurrency())
|
currency := strings.TrimSpace(req.GetCurrency())
|
||||||
if currency == "" {
|
if currency == "" {
|
||||||
return nil, merrors.InvalidArgument("currency is required")
|
return nil, merrors.InvalidArgument("currency is required")
|
||||||
@@ -85,7 +79,12 @@ func (s *Service) createAccountResponder(_ context.Context, req *ledgerv1.Create
|
|||||||
ownerRef = &ownerObjID
|
ownerRef = &ownerObjID
|
||||||
}
|
}
|
||||||
|
|
||||||
account := &model.Account{
|
const maxCreateAttempts = 3
|
||||||
|
var account *model.Account
|
||||||
|
for attempt := 0; attempt < maxCreateAttempts; attempt++ {
|
||||||
|
accountID := primitive.NewObjectID()
|
||||||
|
accountCode := generateAccountCode(modelType, currency, accountID)
|
||||||
|
account = &model.Account{
|
||||||
AccountCode: accountCode,
|
AccountCode: accountCode,
|
||||||
Currency: currency,
|
Currency: currency,
|
||||||
AccountType: modelType,
|
AccountType: modelType,
|
||||||
@@ -99,24 +98,18 @@ func (s *Service) createAccountResponder(_ context.Context, req *ledgerv1.Create
|
|||||||
account.Describable = *describable
|
account.Describable = *describable
|
||||||
}
|
}
|
||||||
account.OrganizationRef = orgRef
|
account.OrganizationRef = orgRef
|
||||||
|
account.SetID(accountID)
|
||||||
|
|
||||||
err = s.storage.Accounts().Create(ctx, account)
|
err = s.storage.Accounts().Create(ctx, account)
|
||||||
if err != nil {
|
if err == nil {
|
||||||
if errors.Is(err, merrors.ErrDataConflict) {
|
recordAccountOperation("create", "success")
|
||||||
existing, lookupErr := s.storage.Accounts().GetByAccountCode(ctx, orgRef, accountCode, currency)
|
|
||||||
if lookupErr != nil {
|
|
||||||
s.logger.Warn("duplicate account create but failed to load existing",
|
|
||||||
zap.Error(lookupErr),
|
|
||||||
mzap.ObjRef("organization_ref", orgRef),
|
|
||||||
zap.String("accountCode", accountCode),
|
|
||||||
zap.String("currency", currency))
|
|
||||||
return nil, merrors.Internal("failed to load existing account after conflict")
|
|
||||||
}
|
|
||||||
recordAccountOperation("create", "duplicate")
|
|
||||||
return &ledgerv1.CreateAccountResponse{
|
return &ledgerv1.CreateAccountResponse{
|
||||||
Account: toProtoAccount(existing),
|
Account: toProtoAccount(account),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
if errors.Is(err, merrors.ErrDataConflict) && attempt < maxCreateAttempts-1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
recordAccountOperation("create", "error")
|
recordAccountOperation("create", "error")
|
||||||
s.logger.Warn("failed to create account",
|
s.logger.Warn("failed to create account",
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
@@ -125,11 +118,8 @@ func (s *Service) createAccountResponder(_ context.Context, req *ledgerv1.Create
|
|||||||
zap.String("currency", currency))
|
zap.String("currency", currency))
|
||||||
return nil, merrors.Internal("failed to create account")
|
return nil, merrors.Internal("failed to create account")
|
||||||
}
|
}
|
||||||
|
recordAccountOperation("create", "error")
|
||||||
recordAccountOperation("create", "success")
|
return nil, merrors.Internal("failed to create account")
|
||||||
return &ledgerv1.CreateAccountResponse{
|
|
||||||
Account: toProtoAccount(account),
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,3 +339,15 @@ func defaultSettlementAccountCode(currency string) string {
|
|||||||
}
|
}
|
||||||
return fmt.Sprintf("asset:settlement:%s", cleaned)
|
return fmt.Sprintf("asset:settlement:%s", cleaned)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func generateAccountCode(accountType model.AccountType, currency string, id primitive.ObjectID) string {
|
||||||
|
typePart := strings.ToLower(strings.TrimSpace(string(accountType)))
|
||||||
|
if typePart == "" {
|
||||||
|
typePart = "account"
|
||||||
|
}
|
||||||
|
currencyPart := strings.ToLower(strings.TrimSpace(currency))
|
||||||
|
if currencyPart == "" {
|
||||||
|
currencyPart = "na"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s:%s:%s", typePart, currencyPart, id.Hex())
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package ledger
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
@@ -23,6 +23,7 @@ type accountStoreStub struct {
|
|||||||
existingErr error
|
existingErr error
|
||||||
defaultSettlement *model.Account
|
defaultSettlement *model.Account
|
||||||
defaultErr error
|
defaultErr error
|
||||||
|
createErrs []error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *accountStoreStub) Create(_ context.Context, account *model.Account) error {
|
func (s *accountStoreStub) Create(_ context.Context, account *model.Account) error {
|
||||||
@@ -30,9 +31,17 @@ func (s *accountStoreStub) Create(_ context.Context, account *model.Account) err
|
|||||||
if s.createErrSettlement != nil {
|
if s.createErrSettlement != nil {
|
||||||
return s.createErrSettlement
|
return s.createErrSettlement
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if len(s.createErrs) > 0 {
|
||||||
|
err := s.createErrs[0]
|
||||||
|
s.createErrs = s.createErrs[1:]
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
} else if s.createErr != nil {
|
} else if s.createErr != nil {
|
||||||
return s.createErr
|
return s.createErr
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if account.GetID() == nil || account.GetID().IsZero() {
|
if account.GetID() == nil || account.GetID().IsZero() {
|
||||||
account.SetID(primitive.NewObjectID())
|
account.SetID(primitive.NewObjectID())
|
||||||
}
|
}
|
||||||
@@ -94,7 +103,6 @@ func TestCreateAccountResponder_Success(t *testing.T) {
|
|||||||
|
|
||||||
req := &ledgerv1.CreateAccountRequest{
|
req := &ledgerv1.CreateAccountRequest{
|
||||||
OrganizationRef: orgRef.Hex(),
|
OrganizationRef: orgRef.Hex(),
|
||||||
AccountCode: "asset:cash:main",
|
|
||||||
AccountType: ledgerv1.AccountType_ACCOUNT_TYPE_ASSET,
|
AccountType: ledgerv1.AccountType_ACCOUNT_TYPE_ASSET,
|
||||||
Currency: "usd",
|
Currency: "usd",
|
||||||
AllowNegative: false,
|
AllowNegative: false,
|
||||||
@@ -107,7 +115,11 @@ func TestCreateAccountResponder_Success(t *testing.T) {
|
|||||||
require.NotNil(t, resp)
|
require.NotNil(t, resp)
|
||||||
require.NotNil(t, resp.Account)
|
require.NotNil(t, resp.Account)
|
||||||
|
|
||||||
require.Equal(t, "asset:cash:main", resp.Account.AccountCode)
|
parts := strings.Split(resp.Account.AccountCode, ":")
|
||||||
|
require.Len(t, parts, 3)
|
||||||
|
require.Equal(t, "asset", parts[0])
|
||||||
|
require.Equal(t, "usd", parts[1])
|
||||||
|
require.Len(t, parts[2], 24)
|
||||||
require.Equal(t, ledgerv1.AccountType_ACCOUNT_TYPE_ASSET, resp.Account.AccountType)
|
require.Equal(t, ledgerv1.AccountType_ACCOUNT_TYPE_ASSET, resp.Account.AccountType)
|
||||||
require.Equal(t, "USD", resp.Account.Currency)
|
require.Equal(t, "USD", resp.Account.Currency)
|
||||||
require.True(t, resp.Account.IsSettlement)
|
require.True(t, resp.Account.IsSettlement)
|
||||||
@@ -129,7 +141,6 @@ func TestCreateAccountResponder_AutoCreatesSettlementAccount(t *testing.T) {
|
|||||||
|
|
||||||
req := &ledgerv1.CreateAccountRequest{
|
req := &ledgerv1.CreateAccountRequest{
|
||||||
OrganizationRef: orgRef.Hex(),
|
OrganizationRef: orgRef.Hex(),
|
||||||
AccountCode: "liability:customer:1",
|
|
||||||
AccountType: ledgerv1.AccountType_ACCOUNT_TYPE_LIABILITY,
|
AccountType: ledgerv1.AccountType_ACCOUNT_TYPE_LIABILITY,
|
||||||
Currency: "usd",
|
Currency: "usd",
|
||||||
}
|
}
|
||||||
@@ -146,40 +157,29 @@ func TestCreateAccountResponder_AutoCreatesSettlementAccount(t *testing.T) {
|
|||||||
if acc.IsSettlement {
|
if acc.IsSettlement {
|
||||||
settlement = acc
|
settlement = acc
|
||||||
}
|
}
|
||||||
if acc.AccountCode == "liability:customer:1" {
|
if !acc.IsSettlement {
|
||||||
created = acc
|
created = acc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
require.NotNil(t, settlement)
|
require.NotNil(t, settlement)
|
||||||
require.NotNil(t, created)
|
require.NotNil(t, created)
|
||||||
|
parts := strings.Split(created.AccountCode, ":")
|
||||||
|
require.Len(t, parts, 3)
|
||||||
|
require.Equal(t, "liability", parts[0])
|
||||||
|
require.Equal(t, "usd", parts[1])
|
||||||
|
require.Len(t, parts[2], 24)
|
||||||
require.Equal(t, defaultSettlementAccountCode("USD"), settlement.AccountCode)
|
require.Equal(t, defaultSettlementAccountCode("USD"), settlement.AccountCode)
|
||||||
require.Equal(t, model.AccountTypeAsset, settlement.AccountType)
|
require.Equal(t, model.AccountTypeAsset, settlement.AccountType)
|
||||||
require.Equal(t, "USD", settlement.Currency)
|
require.Equal(t, "USD", settlement.Currency)
|
||||||
require.True(t, settlement.AllowNegative)
|
require.True(t, settlement.AllowNegative)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCreateAccountResponder_DuplicateReturnsExisting(t *testing.T) {
|
func TestCreateAccountResponder_RetriesOnConflict(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
orgRef := primitive.NewObjectID()
|
orgRef := primitive.NewObjectID()
|
||||||
existing := &model.Account{
|
|
||||||
AccountCode: "asset:cash:main",
|
|
||||||
Currency: "USD",
|
|
||||||
AccountType: model.AccountTypeAsset,
|
|
||||||
Status: model.AccountStatusActive,
|
|
||||||
AllowNegative: false,
|
|
||||||
IsSettlement: true,
|
|
||||||
Metadata: map[string]string{"purpose": "existing"},
|
|
||||||
}
|
|
||||||
existing.OrganizationRef = orgRef
|
|
||||||
existing.SetID(primitive.NewObjectID())
|
|
||||||
existing.CreatedAt = time.Now().Add(-time.Hour).UTC()
|
|
||||||
existing.UpdatedAt = time.Now().UTC()
|
|
||||||
|
|
||||||
accountStore := &accountStoreStub{
|
accountStore := &accountStoreStub{
|
||||||
createErr: merrors.DataConflict("duplicate"),
|
createErrs: []error{merrors.DataConflict("duplicate")},
|
||||||
existing: existing,
|
|
||||||
existingErr: nil,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
svc := &Service{
|
svc := &Service{
|
||||||
@@ -189,7 +189,6 @@ func TestCreateAccountResponder_DuplicateReturnsExisting(t *testing.T) {
|
|||||||
|
|
||||||
req := &ledgerv1.CreateAccountRequest{
|
req := &ledgerv1.CreateAccountRequest{
|
||||||
OrganizationRef: orgRef.Hex(),
|
OrganizationRef: orgRef.Hex(),
|
||||||
AccountCode: "asset:cash:main",
|
|
||||||
AccountType: ledgerv1.AccountType_ACCOUNT_TYPE_ASSET,
|
AccountType: ledgerv1.AccountType_ACCOUNT_TYPE_ASSET,
|
||||||
Currency: "usd",
|
Currency: "usd",
|
||||||
}
|
}
|
||||||
@@ -199,8 +198,15 @@ func TestCreateAccountResponder_DuplicateReturnsExisting(t *testing.T) {
|
|||||||
require.NotNil(t, resp)
|
require.NotNil(t, resp)
|
||||||
require.NotNil(t, resp.Account)
|
require.NotNil(t, resp.Account)
|
||||||
|
|
||||||
require.Equal(t, existing.GetID().Hex(), resp.Account.LedgerAccountRef)
|
require.Len(t, accountStore.created, 2)
|
||||||
require.Equal(t, existing.Metadata["purpose"], resp.Account.Metadata["purpose"])
|
var created *model.Account
|
||||||
|
for _, acc := range accountStore.created {
|
||||||
|
if !acc.IsSettlement {
|
||||||
|
created = acc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
require.NotNil(t, created)
|
||||||
|
require.Equal(t, created.AccountCode, resp.Account.AccountCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCreateAccountResponder_InvalidAccountType(t *testing.T) {
|
func TestCreateAccountResponder_InvalidAccountType(t *testing.T) {
|
||||||
@@ -213,7 +219,6 @@ func TestCreateAccountResponder_InvalidAccountType(t *testing.T) {
|
|||||||
|
|
||||||
req := &ledgerv1.CreateAccountRequest{
|
req := &ledgerv1.CreateAccountRequest{
|
||||||
OrganizationRef: primitive.NewObjectID().Hex(),
|
OrganizationRef: primitive.NewObjectID().Hex(),
|
||||||
AccountCode: "asset:cash:main",
|
|
||||||
Currency: "USD",
|
Currency: "USD",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/tech/sendico/ledger/internal/appversion"
|
"github.com/tech/sendico/ledger/internal/appversion"
|
||||||
"github.com/tech/sendico/pkg/connector/params"
|
"github.com/tech/sendico/pkg/connector/params"
|
||||||
|
"github.com/tech/sendico/pkg/ledgerconv"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1"
|
describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1"
|
||||||
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
|
||||||
@@ -57,9 +58,8 @@ func (c *connectorAdapter) OpenAccount(ctx context.Context, req *connectorv1.Ope
|
|||||||
|
|
||||||
reader := params.New(req.GetParams())
|
reader := params.New(req.GetParams())
|
||||||
orgRef := strings.TrimSpace(reader.String("organization_ref"))
|
orgRef := strings.TrimSpace(reader.String("organization_ref"))
|
||||||
accountCode := strings.TrimSpace(reader.String("account_code"))
|
if orgRef == "" {
|
||||||
if orgRef == "" || accountCode == "" {
|
return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "open_account: organization_ref is required", nil, "")}, nil
|
||||||
return &connectorv1.OpenAccountResponse{Error: connectorError(connectorv1.ErrorCode_INVALID_PARAMS, "open_account: organization_ref and account_code are required", nil, "")}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
accountType, err := parseLedgerAccountType(reader, "account_type")
|
accountType, err := parseLedgerAccountType(reader, "account_type")
|
||||||
@@ -81,7 +81,6 @@ func (c *connectorAdapter) OpenAccount(ctx context.Context, req *connectorv1.Ope
|
|||||||
|
|
||||||
resp, err := c.svc.CreateAccount(ctx, &ledgerv1.CreateAccountRequest{
|
resp, err := c.svc.CreateAccount(ctx, &ledgerv1.CreateAccountRequest{
|
||||||
OrganizationRef: orgRef,
|
OrganizationRef: orgRef,
|
||||||
AccountCode: accountCode,
|
|
||||||
AccountType: accountType,
|
AccountType: accountType,
|
||||||
Currency: currency,
|
Currency: currency,
|
||||||
Status: status,
|
Status: status,
|
||||||
@@ -312,7 +311,6 @@ func (c *connectorAdapter) ListOperations(ctx context.Context, req *connectorv1.
|
|||||||
func ledgerOpenAccountParams() []*connectorv1.ParamSpec {
|
func ledgerOpenAccountParams() []*connectorv1.ParamSpec {
|
||||||
return []*connectorv1.ParamSpec{
|
return []*connectorv1.ParamSpec{
|
||||||
{Key: "organization_ref", Type: connectorv1.ParamType_STRING, Required: true, Description: "Organization reference for the ledger account."},
|
{Key: "organization_ref", Type: connectorv1.ParamType_STRING, Required: true, Description: "Organization reference for the ledger account."},
|
||||||
{Key: "account_code", Type: connectorv1.ParamType_STRING, Required: true, Description: "Ledger account code."},
|
|
||||||
{Key: "account_type", Type: connectorv1.ParamType_STRING, Required: true, Description: "ASSET | LIABILITY | REVENUE | EXPENSE."},
|
{Key: "account_type", Type: connectorv1.ParamType_STRING, Required: true, Description: "ASSET | LIABILITY | REVENUE | EXPENSE."},
|
||||||
{Key: "status", Type: connectorv1.ParamType_STRING, Required: false, Description: "ACTIVE | FROZEN."},
|
{Key: "status", Type: connectorv1.ParamType_STRING, Required: false, Description: "ACTIVE | FROZEN."},
|
||||||
{Key: "allow_negative", Type: connectorv1.ParamType_BOOL, Required: false, Description: "Allow negative balance."},
|
{Key: "allow_negative", Type: connectorv1.ParamType_BOOL, Required: false, Description: "Allow negative balance."},
|
||||||
@@ -550,30 +548,16 @@ func parseLedgerAccountType(reader params.Reader, key string) (ledgerv1.AccountT
|
|||||||
}
|
}
|
||||||
|
|
||||||
func parseLedgerAccountTypeString(value string) (ledgerv1.AccountType, error) {
|
func parseLedgerAccountTypeString(value string) (ledgerv1.AccountType, error) {
|
||||||
switch strings.ToUpper(strings.TrimSpace(value)) {
|
accountType, ok := ledgerconv.ParseAccountType(value)
|
||||||
case "ACCOUNT_TYPE_ASSET", "ASSET":
|
if !ok || ledgerconv.IsAccountTypeUnspecified(value) {
|
||||||
return ledgerv1.AccountType_ACCOUNT_TYPE_ASSET, nil
|
|
||||||
case "ACCOUNT_TYPE_LIABILITY", "LIABILITY":
|
|
||||||
return ledgerv1.AccountType_ACCOUNT_TYPE_LIABILITY, nil
|
|
||||||
case "ACCOUNT_TYPE_REVENUE", "REVENUE":
|
|
||||||
return ledgerv1.AccountType_ACCOUNT_TYPE_REVENUE, nil
|
|
||||||
case "ACCOUNT_TYPE_EXPENSE", "EXPENSE":
|
|
||||||
return ledgerv1.AccountType_ACCOUNT_TYPE_EXPENSE, nil
|
|
||||||
default:
|
|
||||||
return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED, merrors.InvalidArgument("open_account: invalid account_type")
|
return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED, merrors.InvalidArgument("open_account: invalid account_type")
|
||||||
}
|
}
|
||||||
|
return accountType, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseLedgerAccountStatus(reader params.Reader, key string) ledgerv1.AccountStatus {
|
func parseLedgerAccountStatus(reader params.Reader, key string) ledgerv1.AccountStatus {
|
||||||
value := strings.ToUpper(strings.TrimSpace(reader.String(key)))
|
status, _ := ledgerconv.ParseAccountStatus(reader.String(key))
|
||||||
switch value {
|
return status
|
||||||
case "ACCOUNT_STATUS_ACTIVE", "ACTIVE":
|
|
||||||
return ledgerv1.AccountStatus_ACCOUNT_STATUS_ACTIVE
|
|
||||||
case "ACCOUNT_STATUS_FROZEN", "FROZEN":
|
|
||||||
return ledgerv1.AccountStatus_ACCOUNT_STATUS_FROZEN
|
|
||||||
default:
|
|
||||||
return ledgerv1.AccountStatus_ACCOUNT_STATUS_UNSPECIFIED
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseEventTime(reader params.Reader) *timestamppb.Timestamp {
|
func parseEventTime(reader params.Reader) *timestamppb.Timestamp {
|
||||||
|
|||||||
55
api/pkg/ledgerconv/account.go
Normal file
55
api/pkg/ledgerconv/account.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package ledgerconv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseAccountType(value string) (ledgerv1.AccountType, bool) {
|
||||||
|
switch strings.ToUpper(strings.TrimSpace(value)) {
|
||||||
|
case "ACCOUNT_TYPE_ASSET", "ASSET":
|
||||||
|
return ledgerv1.AccountType_ACCOUNT_TYPE_ASSET, true
|
||||||
|
case "ACCOUNT_TYPE_LIABILITY", "LIABILITY":
|
||||||
|
return ledgerv1.AccountType_ACCOUNT_TYPE_LIABILITY, true
|
||||||
|
case "ACCOUNT_TYPE_REVENUE", "REVENUE":
|
||||||
|
return ledgerv1.AccountType_ACCOUNT_TYPE_REVENUE, true
|
||||||
|
case "ACCOUNT_TYPE_EXPENSE", "EXPENSE":
|
||||||
|
return ledgerv1.AccountType_ACCOUNT_TYPE_EXPENSE, true
|
||||||
|
case "ACCOUNT_TYPE_UNSPECIFIED", "UNSPECIFIED":
|
||||||
|
return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED, true
|
||||||
|
default:
|
||||||
|
return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseAccountStatus(value string) (ledgerv1.AccountStatus, bool) {
|
||||||
|
switch strings.ToUpper(strings.TrimSpace(value)) {
|
||||||
|
case "ACCOUNT_STATUS_ACTIVE", "ACTIVE":
|
||||||
|
return ledgerv1.AccountStatus_ACCOUNT_STATUS_ACTIVE, true
|
||||||
|
case "ACCOUNT_STATUS_FROZEN", "FROZEN":
|
||||||
|
return ledgerv1.AccountStatus_ACCOUNT_STATUS_FROZEN, true
|
||||||
|
case "ACCOUNT_STATUS_UNSPECIFIED", "UNSPECIFIED":
|
||||||
|
return ledgerv1.AccountStatus_ACCOUNT_STATUS_UNSPECIFIED, true
|
||||||
|
default:
|
||||||
|
return ledgerv1.AccountStatus_ACCOUNT_STATUS_UNSPECIFIED, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsAccountTypeUnspecified(value string) bool {
|
||||||
|
switch strings.ToUpper(strings.TrimSpace(value)) {
|
||||||
|
case "", "ACCOUNT_TYPE_UNSPECIFIED", "UNSPECIFIED":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsAccountStatusUnspecified(value string) bool {
|
||||||
|
switch strings.ToUpper(strings.TrimSpace(value)) {
|
||||||
|
case "", "ACCOUNT_STATUS_UNSPECIFIED", "UNSPECIFIED":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -72,7 +72,8 @@ message PostingLine {
|
|||||||
message CreateAccountRequest {
|
message CreateAccountRequest {
|
||||||
string organization_ref = 1;
|
string organization_ref = 1;
|
||||||
string owner_ref = 2;
|
string owner_ref = 2;
|
||||||
string account_code = 3;
|
reserved 3;
|
||||||
|
reserved "account_code";
|
||||||
AccountType account_type = 4;
|
AccountType account_type = 4;
|
||||||
string currency = 5;
|
string currency = 5;
|
||||||
AccountStatus status = 6;
|
AccountStatus status = 6;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
"github.com/tech/sendico/pkg/model"
|
"github.com/tech/sendico/pkg/model"
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LedgerAccountType string
|
type LedgerAccountType string
|
||||||
@@ -26,21 +27,16 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type CreateLedgerAccount struct {
|
type CreateLedgerAccount struct {
|
||||||
AccountCode string `json:"accountCode"`
|
|
||||||
AccountType LedgerAccountType `json:"accountType"`
|
AccountType LedgerAccountType `json:"accountType"`
|
||||||
Currency string `json:"currency"`
|
Currency string `json:"currency"`
|
||||||
Status LedgerAccountStatus `json:"status,omitempty"`
|
|
||||||
AllowNegative bool `json:"allowNegative,omitempty"`
|
AllowNegative bool `json:"allowNegative,omitempty"`
|
||||||
IsSettlement bool `json:"isSettlement,omitempty"`
|
IsSettlement bool `json:"isSettlement,omitempty"`
|
||||||
Metadata map[string]string `json:"metadata,omitempty"`
|
|
||||||
Describable model.Describable `json:"describable"`
|
Describable model.Describable `json:"describable"`
|
||||||
IsOrgWallet bool `json:"isOrgWallet"`
|
OwnerRef *primitive.ObjectID `json:"ownerRef,omitempty"`
|
||||||
|
Metadata map[string]string `json:"metadata,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *CreateLedgerAccount) Validate() error {
|
func (r *CreateLedgerAccount) Validate() error {
|
||||||
if strings.TrimSpace(r.AccountCode) == "" {
|
|
||||||
return merrors.InvalidArgument("accountCode is required", "accountCode")
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(r.Currency) == "" {
|
if strings.TrimSpace(r.Currency) == "" {
|
||||||
return merrors.InvalidArgument("currency is required", "currency")
|
return merrors.InvalidArgument("currency is required", "currency")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
package srequest
|
package srequest
|
||||||
|
|
||||||
import "github.com/tech/sendico/pkg/model"
|
import (
|
||||||
|
"github.com/tech/sendico/pkg/model"
|
||||||
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||||
|
)
|
||||||
|
|
||||||
type CreateWallet struct {
|
type CreateWallet struct {
|
||||||
Description model.Describable `json:"description"`
|
Description model.Describable `json:"description"`
|
||||||
IsOrgWallet bool `json:"isOrgWallet"`
|
|
||||||
Asset model.ChainAssetKey `json:"asset"`
|
Asset model.ChainAssetKey `json:"asset"`
|
||||||
|
OwnerRef *primitive.ObjectID `json:"ownerRef,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/tech/sendico/pkg/api/http/response"
|
"github.com/tech/sendico/pkg/api/http/response"
|
||||||
|
"github.com/tech/sendico/pkg/ledgerconv"
|
||||||
"github.com/tech/sendico/pkg/merrors"
|
"github.com/tech/sendico/pkg/merrors"
|
||||||
"github.com/tech/sendico/pkg/model"
|
"github.com/tech/sendico/pkg/model"
|
||||||
"github.com/tech/sendico/pkg/mservice"
|
"github.com/tech/sendico/pkg/mservice"
|
||||||
@@ -46,11 +47,6 @@ func (a *LedgerAPI) createAccount(r *http.Request, account *model.Account, token
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return response.BadPayload(a.logger, a.Name(), err)
|
return response.BadPayload(a.logger, a.Name(), err)
|
||||||
}
|
}
|
||||||
status, err := mapLedgerAccountStatus(payload.Status)
|
|
||||||
if err != nil {
|
|
||||||
return response.BadPayload(a.logger, a.Name(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if a.client == nil {
|
if a.client == nil {
|
||||||
return response.Internal(a.logger, mservice.Ledger, merrors.Internal("ledger client is not configured"))
|
return response.Internal(a.logger, mservice.Ledger, merrors.Internal("ledger client is not configured"))
|
||||||
}
|
}
|
||||||
@@ -71,17 +67,16 @@ func (a *LedgerAPI) createAccount(r *http.Request, account *model.Account, token
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
var ownerRef string
|
var ownerRef string
|
||||||
if !payload.IsOrgWallet {
|
if payload.OwnerRef != nil && !payload.OwnerRef.IsZero() {
|
||||||
ownerRef = account.ID.Hex()
|
ownerRef = payload.OwnerRef.Hex()
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := a.client.CreateAccount(ctx, &ledgerv1.CreateAccountRequest{
|
resp, err := a.client.CreateAccount(ctx, &ledgerv1.CreateAccountRequest{
|
||||||
OrganizationRef: orgRef.Hex(),
|
OrganizationRef: orgRef.Hex(),
|
||||||
OwnerRef: ownerRef,
|
OwnerRef: ownerRef,
|
||||||
AccountCode: payload.AccountCode,
|
|
||||||
AccountType: accountType,
|
AccountType: accountType,
|
||||||
Currency: payload.Currency,
|
Currency: payload.Currency,
|
||||||
Status: status,
|
Status: ledgerv1.AccountStatus_ACCOUNT_STATUS_ACTIVE,
|
||||||
AllowNegative: payload.AllowNegative,
|
AllowNegative: payload.AllowNegative,
|
||||||
IsSettlement: payload.IsSettlement,
|
IsSettlement: payload.IsSettlement,
|
||||||
Metadata: payload.Metadata,
|
Metadata: payload.Metadata,
|
||||||
@@ -102,7 +97,6 @@ func decodeLedgerAccountCreatePayload(r *http.Request) (*srequest.CreateLedgerAc
|
|||||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||||
return nil, merrors.InvalidArgument("invalid payload: " + err.Error())
|
return nil, merrors.InvalidArgument("invalid payload: " + err.Error())
|
||||||
}
|
}
|
||||||
payload.AccountCode = strings.TrimSpace(payload.AccountCode)
|
|
||||||
payload.Currency = strings.ToUpper(strings.TrimSpace(payload.Currency))
|
payload.Currency = strings.ToUpper(strings.TrimSpace(payload.Currency))
|
||||||
payload.Describable.Name = strings.TrimSpace(payload.Describable.Name)
|
payload.Describable.Name = strings.TrimSpace(payload.Describable.Name)
|
||||||
if payload.Describable.Description != nil {
|
if payload.Describable.Description != nil {
|
||||||
@@ -123,31 +117,25 @@ func decodeLedgerAccountCreatePayload(r *http.Request) (*srequest.CreateLedgerAc
|
|||||||
}
|
}
|
||||||
|
|
||||||
func mapLedgerAccountType(accountType srequest.LedgerAccountType) (ledgerv1.AccountType, error) {
|
func mapLedgerAccountType(accountType srequest.LedgerAccountType) (ledgerv1.AccountType, error) {
|
||||||
switch strings.ToUpper(strings.TrimSpace(string(accountType))) {
|
raw := string(accountType)
|
||||||
case "ACCOUNT_TYPE_ASSET", "ASSET":
|
if ledgerconv.IsAccountTypeUnspecified(raw) {
|
||||||
return ledgerv1.AccountType_ACCOUNT_TYPE_ASSET, nil
|
|
||||||
case "ACCOUNT_TYPE_LIABILITY", "LIABILITY":
|
|
||||||
return ledgerv1.AccountType_ACCOUNT_TYPE_LIABILITY, nil
|
|
||||||
case "ACCOUNT_TYPE_REVENUE", "REVENUE":
|
|
||||||
return ledgerv1.AccountType_ACCOUNT_TYPE_REVENUE, nil
|
|
||||||
case "ACCOUNT_TYPE_EXPENSE", "EXPENSE":
|
|
||||||
return ledgerv1.AccountType_ACCOUNT_TYPE_EXPENSE, nil
|
|
||||||
case "", "ACCOUNT_TYPE_UNSPECIFIED", "UNSPECIFIED":
|
|
||||||
return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED, merrors.InvalidArgument("accountType is required", "accountType")
|
return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED, merrors.InvalidArgument("accountType is required", "accountType")
|
||||||
default:
|
}
|
||||||
|
parsed, ok := ledgerconv.ParseAccountType(raw)
|
||||||
|
if !ok {
|
||||||
return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED, merrors.InvalidArgument("unsupported accountType: "+string(accountType), "accountType")
|
return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED, merrors.InvalidArgument("unsupported accountType: "+string(accountType), "accountType")
|
||||||
}
|
}
|
||||||
|
return parsed, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func mapLedgerAccountStatus(status srequest.LedgerAccountStatus) (ledgerv1.AccountStatus, error) {
|
func mapLedgerAccountStatus(status srequest.LedgerAccountStatus) (ledgerv1.AccountStatus, error) {
|
||||||
switch strings.ToUpper(strings.TrimSpace(string(status))) {
|
raw := string(status)
|
||||||
case "", "ACCOUNT_STATUS_UNSPECIFIED", "UNSPECIFIED":
|
if ledgerconv.IsAccountStatusUnspecified(raw) {
|
||||||
return ledgerv1.AccountStatus_ACCOUNT_STATUS_UNSPECIFIED, nil
|
return ledgerv1.AccountStatus_ACCOUNT_STATUS_UNSPECIFIED, nil
|
||||||
case "ACCOUNT_STATUS_ACTIVE", "ACTIVE":
|
}
|
||||||
return ledgerv1.AccountStatus_ACCOUNT_STATUS_ACTIVE, nil
|
parsed, ok := ledgerconv.ParseAccountStatus(raw)
|
||||||
case "ACCOUNT_STATUS_FROZEN", "FROZEN":
|
if !ok {
|
||||||
return ledgerv1.AccountStatus_ACCOUNT_STATUS_FROZEN, nil
|
|
||||||
default:
|
|
||||||
return ledgerv1.AccountStatus_ACCOUNT_STATUS_UNSPECIFIED, merrors.InvalidArgument("unsupported status: "+string(status), "status")
|
return ledgerv1.AccountStatus_ACCOUNT_STATUS_UNSPECIFIED, merrors.InvalidArgument("unsupported status: "+string(status), "status")
|
||||||
}
|
}
|
||||||
|
return parsed, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,8 +57,8 @@ func (a *WalletAPI) create(r *http.Request, account *model.Account, token *sresp
|
|||||||
}
|
}
|
||||||
|
|
||||||
var ownerRef string
|
var ownerRef string
|
||||||
if !sr.IsOrgWallet {
|
if sr.OwnerRef != nil && !sr.OwnerRef.IsZero() {
|
||||||
ownerRef = account.ID.Hex()
|
ownerRef = sr.OwnerRef.Hex()
|
||||||
}
|
}
|
||||||
passet, err := ast.Asset2Proto(&asset.Asset)
|
passet, err := ast.Asset2Proto(&asset.Asset)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
32
frontend/pshared/lib/api/requests/ledger/create.dart
Normal file
32
frontend/pshared/lib/api/requests/ledger/create.dart
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
import 'package:pshared/data/dto/describable.dart';
|
||||||
|
import 'package:pshared/data/dto/ledger/type.dart';
|
||||||
|
|
||||||
|
part 'create.g.dart';
|
||||||
|
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class CreateLedgerAccountRequest {
|
||||||
|
final Map<String, String>? metadata;
|
||||||
|
final String currency;
|
||||||
|
final bool allowNegative;
|
||||||
|
final bool isSettlement;
|
||||||
|
final DescribableDTO describable;
|
||||||
|
final String? ownerRef;
|
||||||
|
final LedgerAccountTypeDTO accountType;
|
||||||
|
|
||||||
|
const CreateLedgerAccountRequest({
|
||||||
|
this.metadata,
|
||||||
|
required this.currency,
|
||||||
|
required this.allowNegative,
|
||||||
|
required this.isSettlement,
|
||||||
|
required this.describable,
|
||||||
|
required this.accountType,
|
||||||
|
this.ownerRef,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory CreateLedgerAccountRequest.fromJson(Map<String, dynamic> json) => _$CreateLedgerAccountRequestFromJson(json);
|
||||||
|
Map<String, dynamic> toJson() => _$CreateLedgerAccountRequestToJson(this);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
|
||||||
part 'base.g.dart';
|
part 'base.g.dart';
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
25
frontend/pshared/lib/api/requests/wallet/create.dart
Normal file
25
frontend/pshared/lib/api/requests/wallet/create.dart
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
import 'package:pshared/data/dto/describable.dart';
|
||||||
|
import 'package:pshared/data/dto/wallet/chain_asset.dart';
|
||||||
|
|
||||||
|
part 'create.g.dart';
|
||||||
|
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class CreateWalletRequest {
|
||||||
|
final Map<String, String>? metadata;
|
||||||
|
final DescribableDTO describable;
|
||||||
|
final String? ownerRef;
|
||||||
|
final ChainAssetDTO asset;
|
||||||
|
|
||||||
|
const CreateWalletRequest({
|
||||||
|
this.metadata,
|
||||||
|
required this.asset,
|
||||||
|
required this.describable,
|
||||||
|
this.ownerRef,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory CreateWalletRequest.fromJson(Map<String, dynamic> json) => _$CreateWalletRequestFromJson(json);
|
||||||
|
Map<String, dynamic> toJson() => _$CreateWalletRequestToJson(this);
|
||||||
|
}
|
||||||
@@ -60,22 +60,23 @@ class WalletsController with ChangeNotifier {
|
|||||||
|
|
||||||
String? get selectedWalletRef => _selectedWalletRef;
|
String? get selectedWalletRef => _selectedWalletRef;
|
||||||
|
|
||||||
void selectWallet(Wallet wallet) => selectWalletByRef(wallet.id);
|
void selectWallet(Wallet wallet, {bool allowHidden = false}) =>
|
||||||
|
selectWalletByRef(wallet.id, allowHidden: allowHidden);
|
||||||
|
|
||||||
void selectWalletByRef(String walletRef) {
|
void selectWalletByRef(String walletRef, {bool allowHidden = false}) {
|
||||||
if (_selectedWalletRef == walletRef) return;
|
if (_selectedWalletRef == walletRef) return;
|
||||||
|
|
||||||
// Prevent selecting a hidden wallet
|
// Prevent selecting a hidden wallet
|
||||||
if (!_visibleWalletRefs.contains(walletRef)) return;
|
if (!allowHidden && !_visibleWalletRefs.contains(walletRef)) return;
|
||||||
|
|
||||||
_selectedWalletRef = walletRef;
|
_selectedWalletRef = walletRef;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Toggle wallet visibility
|
/// Toggle wallet visibility
|
||||||
void toggleVisibility(String walletId) {
|
void toggleVisibility(String accountRef) {
|
||||||
final existed = _visibleWalletRefs.remove(walletId);
|
final existed = _visibleWalletRefs.remove(accountRef);
|
||||||
if (!existed) _visibleWalletRefs.add(walletId);
|
if (!existed) _visibleWalletRefs.add(accountRef);
|
||||||
|
|
||||||
_selectedWalletRef = _resolveSelectedId(
|
_selectedWalletRef = _resolveSelectedId(
|
||||||
currentRef: _selectedWalletRef,
|
currentRef: _selectedWalletRef,
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import 'package:json_annotation/json_annotation.dart';
|
|||||||
|
|
||||||
import 'package:pshared/data/dto/describable.dart';
|
import 'package:pshared/data/dto/describable.dart';
|
||||||
import 'package:pshared/data/dto/ledger/balance.dart';
|
import 'package:pshared/data/dto/ledger/balance.dart';
|
||||||
|
import 'package:pshared/data/dto/ledger/status.dart';
|
||||||
|
import 'package:pshared/data/dto/ledger/type.dart';
|
||||||
|
|
||||||
part 'account.g.dart';
|
part 'account.g.dart';
|
||||||
|
|
||||||
@@ -12,9 +14,9 @@ class LedgerAccountDTO {
|
|||||||
final String organizationRef;
|
final String organizationRef;
|
||||||
final String? ownerRef;
|
final String? ownerRef;
|
||||||
final String accountCode;
|
final String accountCode;
|
||||||
final String accountType;
|
final LedgerAccountTypeDTO accountType;
|
||||||
final String currency;
|
final String currency;
|
||||||
final String status;
|
final LedgerAccountStatusDTO status;
|
||||||
final bool allowNegative;
|
final bool allowNegative;
|
||||||
final bool isSettlement;
|
final bool isSettlement;
|
||||||
final Map<String, String>? metadata;
|
final Map<String, String>? metadata;
|
||||||
|
|||||||
13
frontend/pshared/lib/data/dto/ledger/status.dart
Normal file
13
frontend/pshared/lib/data/dto/ledger/status.dart
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
|
||||||
|
enum LedgerAccountStatusDTO {
|
||||||
|
@JsonValue('unspecified')
|
||||||
|
unspecified,
|
||||||
|
|
||||||
|
@JsonValue('active')
|
||||||
|
active,
|
||||||
|
|
||||||
|
@JsonValue('frozen')
|
||||||
|
frozen,
|
||||||
|
}
|
||||||
19
frontend/pshared/lib/data/dto/ledger/type.dart
Normal file
19
frontend/pshared/lib/data/dto/ledger/type.dart
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
|
||||||
|
enum LedgerAccountTypeDTO {
|
||||||
|
@JsonValue('unspecified')
|
||||||
|
unspecified,
|
||||||
|
|
||||||
|
@JsonValue('asset')
|
||||||
|
asset,
|
||||||
|
|
||||||
|
@JsonValue('liability')
|
||||||
|
liability,
|
||||||
|
|
||||||
|
@JsonValue('revenue')
|
||||||
|
revenue,
|
||||||
|
|
||||||
|
@JsonValue('expense')
|
||||||
|
expense,
|
||||||
|
}
|
||||||
@@ -1,20 +1,21 @@
|
|||||||
import 'package:json_annotation/json_annotation.dart';
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
import 'package:pshared/data/dto/wallet/chain_asset.dart';
|
||||||
|
|
||||||
part 'asset.g.dart';
|
part 'asset.g.dart';
|
||||||
|
|
||||||
|
|
||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class WalletAssetDTO {
|
class WalletAssetDTO extends ChainAssetDTO {
|
||||||
final String chain;
|
|
||||||
final String tokenSymbol;
|
|
||||||
final String contractAddress;
|
final String contractAddress;
|
||||||
|
|
||||||
const WalletAssetDTO({
|
const WalletAssetDTO({
|
||||||
required this.chain,
|
required super.chain,
|
||||||
required this.tokenSymbol,
|
required super.tokenSymbol,
|
||||||
required this.contractAddress,
|
required this.contractAddress,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory WalletAssetDTO.fromJson(Map<String, dynamic> json) => _$WalletAssetDTOFromJson(json);
|
factory WalletAssetDTO.fromJson(Map<String, dynamic> json) => _$WalletAssetDTOFromJson(json);
|
||||||
|
@override
|
||||||
Map<String, dynamic> toJson() => _$WalletAssetDTOToJson(this);
|
Map<String, dynamic> toJson() => _$WalletAssetDTOToJson(this);
|
||||||
}
|
}
|
||||||
|
|||||||
18
frontend/pshared/lib/data/dto/wallet/chain_asset.dart
Normal file
18
frontend/pshared/lib/data/dto/wallet/chain_asset.dart
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
|
||||||
|
part 'chain_asset.g.dart';
|
||||||
|
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class ChainAssetDTO {
|
||||||
|
final String chain;
|
||||||
|
final String tokenSymbol;
|
||||||
|
|
||||||
|
const ChainAssetDTO({
|
||||||
|
required this.chain,
|
||||||
|
required this.tokenSymbol,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory ChainAssetDTO.fromJson(Map<String, dynamic> json) => _$ChainAssetDTOFromJson(json);
|
||||||
|
Map<String, dynamic> toJson() => _$ChainAssetDTOToJson(this);
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'package:pshared/data/dto/ledger/account.dart';
|
import 'package:pshared/data/dto/ledger/account.dart';
|
||||||
import 'package:pshared/data/mapper/describable.dart';
|
import 'package:pshared/data/mapper/describable.dart';
|
||||||
import 'package:pshared/data/mapper/ledger/balance.dart';
|
import 'package:pshared/data/mapper/ledger/balance.dart';
|
||||||
|
import 'package:pshared/data/mapper/ledger/status.dart';
|
||||||
|
import 'package:pshared/data/mapper/ledger/type.dart';
|
||||||
import 'package:pshared/models/ledger/account.dart';
|
import 'package:pshared/models/ledger/account.dart';
|
||||||
|
|
||||||
|
|
||||||
@@ -10,9 +12,9 @@ extension LedgerAccountDTOMapper on LedgerAccountDTO {
|
|||||||
organizationRef: organizationRef,
|
organizationRef: organizationRef,
|
||||||
ownerRef: ownerRef,
|
ownerRef: ownerRef,
|
||||||
accountCode: accountCode,
|
accountCode: accountCode,
|
||||||
accountType: accountType,
|
accountType: accountType.toDomain(),
|
||||||
currency: currency,
|
currency: currency,
|
||||||
status: status,
|
status: status.toDomain(),
|
||||||
allowNegative: allowNegative,
|
allowNegative: allowNegative,
|
||||||
isSettlement: isSettlement,
|
isSettlement: isSettlement,
|
||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
@@ -29,9 +31,9 @@ extension LedgerAccountModelMapper on LedgerAccount {
|
|||||||
organizationRef: organizationRef,
|
organizationRef: organizationRef,
|
||||||
ownerRef: ownerRef,
|
ownerRef: ownerRef,
|
||||||
accountCode: accountCode,
|
accountCode: accountCode,
|
||||||
accountType: accountType,
|
accountType: accountType.toDTO(),
|
||||||
currency: currency,
|
currency: currency,
|
||||||
status: status,
|
status: status.toDTO(),
|
||||||
allowNegative: allowNegative,
|
allowNegative: allowNegative,
|
||||||
isSettlement: isSettlement,
|
isSettlement: isSettlement,
|
||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
|
|||||||
29
frontend/pshared/lib/data/mapper/ledger/status.dart
Normal file
29
frontend/pshared/lib/data/mapper/ledger/status.dart
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import 'package:pshared/data/dto/ledger/status.dart';
|
||||||
|
import 'package:pshared/models/ledger/status.dart';
|
||||||
|
|
||||||
|
|
||||||
|
extension LedgerAccountStatusDTOMapper on LedgerAccountStatusDTO {
|
||||||
|
LedgerAccountStatus toDomain() {
|
||||||
|
switch (this) {
|
||||||
|
case LedgerAccountStatusDTO.unspecified:
|
||||||
|
return LedgerAccountStatus.unspecified;
|
||||||
|
case LedgerAccountStatusDTO.active:
|
||||||
|
return LedgerAccountStatus.active;
|
||||||
|
case LedgerAccountStatusDTO.frozen:
|
||||||
|
return LedgerAccountStatus.frozen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension LedgerAccountStatusMapper on LedgerAccountStatus {
|
||||||
|
LedgerAccountStatusDTO toDTO() {
|
||||||
|
switch (this) {
|
||||||
|
case LedgerAccountStatus.unspecified:
|
||||||
|
return LedgerAccountStatusDTO.unspecified;
|
||||||
|
case LedgerAccountStatus.active:
|
||||||
|
return LedgerAccountStatusDTO.active;
|
||||||
|
case LedgerAccountStatus.frozen:
|
||||||
|
return LedgerAccountStatusDTO.frozen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
frontend/pshared/lib/data/mapper/ledger/type.dart
Normal file
37
frontend/pshared/lib/data/mapper/ledger/type.dart
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import 'package:pshared/data/dto/ledger/type.dart';
|
||||||
|
import 'package:pshared/models/ledger/type.dart';
|
||||||
|
|
||||||
|
|
||||||
|
extension LedgerAccountTypeDTOMapper on LedgerAccountTypeDTO {
|
||||||
|
LedgerAccountType toDomain() {
|
||||||
|
switch (this) {
|
||||||
|
case LedgerAccountTypeDTO.unspecified:
|
||||||
|
return LedgerAccountType.unspecified;
|
||||||
|
case LedgerAccountTypeDTO.asset:
|
||||||
|
return LedgerAccountType.asset;
|
||||||
|
case LedgerAccountTypeDTO.liability:
|
||||||
|
return LedgerAccountType.liability;
|
||||||
|
case LedgerAccountTypeDTO.revenue:
|
||||||
|
return LedgerAccountType.revenue;
|
||||||
|
case LedgerAccountTypeDTO.expense:
|
||||||
|
return LedgerAccountType.expense;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension LedgerAccountTypeModelMapper on LedgerAccountType {
|
||||||
|
LedgerAccountTypeDTO toDTO() {
|
||||||
|
switch (this) {
|
||||||
|
case LedgerAccountType.unspecified:
|
||||||
|
return LedgerAccountTypeDTO.unspecified;
|
||||||
|
case LedgerAccountType.asset:
|
||||||
|
return LedgerAccountTypeDTO.asset;
|
||||||
|
case LedgerAccountType.liability:
|
||||||
|
return LedgerAccountTypeDTO.liability;
|
||||||
|
case LedgerAccountType.revenue:
|
||||||
|
return LedgerAccountTypeDTO.revenue;
|
||||||
|
case LedgerAccountType.expense:
|
||||||
|
return LedgerAccountTypeDTO.expense;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
frontend/pshared/lib/data/mapper/wallet/chain_asset.dart
Normal file
18
frontend/pshared/lib/data/mapper/wallet/chain_asset.dart
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import 'package:pshared/data/dto/wallet/chain_asset.dart';
|
||||||
|
import 'package:pshared/data/mapper/payment/enums.dart';
|
||||||
|
import 'package:pshared/models/wallet/chain_asset.dart';
|
||||||
|
|
||||||
|
|
||||||
|
extension ChainAssetDTOMapper on ChainAssetDTO {
|
||||||
|
ChainAsset toDomain() => ChainAsset(
|
||||||
|
chain: chainNetworkFromValue(chain),
|
||||||
|
tokenSymbol: tokenSymbol,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ChainAssetMapper on ChainAsset {
|
||||||
|
ChainAssetDTO toDTO() => ChainAssetDTO(
|
||||||
|
chain: chainNetworkToValue(chain),
|
||||||
|
tokenSymbol: tokenSymbol,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import 'package:pshared/models/describable.dart';
|
import 'package:pshared/models/describable.dart';
|
||||||
import 'package:pshared/models/ledger/balance.dart';
|
import 'package:pshared/models/ledger/balance.dart';
|
||||||
|
import 'package:pshared/models/ledger/status.dart';
|
||||||
|
import 'package:pshared/models/ledger/type.dart';
|
||||||
|
|
||||||
|
|
||||||
class LedgerAccount implements Describable {
|
class LedgerAccount implements Describable {
|
||||||
@@ -7,9 +9,9 @@ class LedgerAccount implements Describable {
|
|||||||
final String organizationRef;
|
final String organizationRef;
|
||||||
final String? ownerRef;
|
final String? ownerRef;
|
||||||
final String accountCode;
|
final String accountCode;
|
||||||
final String accountType;
|
final LedgerAccountType accountType;
|
||||||
final String currency;
|
final String currency;
|
||||||
final String status;
|
final LedgerAccountStatus status;
|
||||||
final bool allowNegative;
|
final bool allowNegative;
|
||||||
final bool isSettlement;
|
final bool isSettlement;
|
||||||
final Map<String, String>? metadata;
|
final Map<String, String>? metadata;
|
||||||
|
|||||||
5
frontend/pshared/lib/models/ledger/status.dart
Normal file
5
frontend/pshared/lib/models/ledger/status.dart
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
enum LedgerAccountStatus {
|
||||||
|
unspecified,
|
||||||
|
active,
|
||||||
|
frozen,
|
||||||
|
}
|
||||||
7
frontend/pshared/lib/models/ledger/type.dart
Normal file
7
frontend/pshared/lib/models/ledger/type.dart
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
enum LedgerAccountType {
|
||||||
|
unspecified,
|
||||||
|
asset,
|
||||||
|
liability,
|
||||||
|
revenue,
|
||||||
|
expense,
|
||||||
|
}
|
||||||
@@ -1,14 +1,12 @@
|
|||||||
import 'package:pshared/models/payment/chain_network.dart';
|
import 'package:pshared/models/wallet/chain_asset.dart';
|
||||||
|
|
||||||
|
|
||||||
class WalletAsset {
|
class WalletAsset extends ChainAsset {
|
||||||
final ChainNetwork chain;
|
|
||||||
final String tokenSymbol;
|
|
||||||
final String contractAddress;
|
final String contractAddress;
|
||||||
|
|
||||||
const WalletAsset({
|
const WalletAsset({
|
||||||
required this.chain,
|
required super.chain,
|
||||||
required this.tokenSymbol,
|
required super.tokenSymbol,
|
||||||
required this.contractAddress,
|
required this.contractAddress,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
12
frontend/pshared/lib/models/wallet/chain_asset.dart
Normal file
12
frontend/pshared/lib/models/wallet/chain_asset.dart
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import 'package:pshared/models/payment/chain_network.dart';
|
||||||
|
|
||||||
|
|
||||||
|
class ChainAsset {
|
||||||
|
final ChainNetwork chain;
|
||||||
|
final String tokenSymbol;
|
||||||
|
|
||||||
|
const ChainAsset({
|
||||||
|
required this.chain,
|
||||||
|
required this.tokenSymbol,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import 'package:pshared/models/organization/employee.dart';
|
|||||||
import 'package:pshared/provider/organizations.dart';
|
import 'package:pshared/provider/organizations.dart';
|
||||||
import 'package:pshared/provider/resource.dart';
|
import 'package:pshared/provider/resource.dart';
|
||||||
import 'package:pshared/service/accounts/employees.dart';
|
import 'package:pshared/service/accounts/employees.dart';
|
||||||
|
import 'package:pshared/utils/exception.dart';
|
||||||
|
|
||||||
|
|
||||||
class EmployeesProvider extends ChangeNotifier {
|
class EmployeesProvider extends ChangeNotifier {
|
||||||
@@ -46,10 +47,7 @@ class EmployeesProvider extends ChangeNotifier {
|
|||||||
error: null,
|
error: null,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_employees = _employees.copyWith(
|
_employees = _employees.copyWith(error: toException(e), isLoading: false);
|
||||||
error: e is Exception ? e : Exception('Unknown error: ${e.toString()}'),
|
|
||||||
isLoading: false,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ import 'dart:math';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:pshared/models/ledger/account.dart';
|
|
||||||
|
|
||||||
|
import 'package:pshared/models/currency.dart';
|
||||||
|
import 'package:pshared/models/describable.dart';
|
||||||
|
import 'package:pshared/models/ledger/account.dart';
|
||||||
import 'package:pshared/models/payment/wallet.dart';
|
import 'package:pshared/models/payment/wallet.dart';
|
||||||
import 'package:pshared/provider/organizations.dart';
|
import 'package:pshared/provider/organizations.dart';
|
||||||
import 'package:pshared/provider/resource.dart';
|
import 'package:pshared/provider/resource.dart';
|
||||||
@@ -159,6 +161,30 @@ class LedgerAccountsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> create({
|
||||||
|
required Describable describable,
|
||||||
|
required Currency currency,
|
||||||
|
String? ownerRef,
|
||||||
|
}) async {
|
||||||
|
final org = _organizations;
|
||||||
|
if (org == null || !org.isOrganizationSet) return;
|
||||||
|
|
||||||
|
_applyResource(_resource.copyWith(isLoading: true, error: null), notify: true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _service.create(
|
||||||
|
organizationRef: org.current.id,
|
||||||
|
currency: currency,
|
||||||
|
describable: describable,
|
||||||
|
ownerRef: ownerRef,
|
||||||
|
);
|
||||||
|
await loadAccountsWithBalances();
|
||||||
|
} catch (e) {
|
||||||
|
_applyResource(_resource.copyWith(isLoading: false, error: toException(e)), notify: true);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- internals ----------
|
// ---------- internals ----------
|
||||||
|
|
||||||
void _applyResource(Resource<List<LedgerAccount>> newResource, {required bool notify}) {
|
void _applyResource(Resource<List<LedgerAccount>> newResource, {required bool notify}) {
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import 'package:flutter/foundation.dart';
|
|||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
|
import 'package:pshared/models/describable.dart';
|
||||||
import 'package:pshared/models/payment/wallet.dart';
|
import 'package:pshared/models/payment/wallet.dart';
|
||||||
|
import 'package:pshared/models/wallet/chain_asset.dart';
|
||||||
import 'package:pshared/provider/organizations.dart';
|
import 'package:pshared/provider/organizations.dart';
|
||||||
import 'package:pshared/provider/resource.dart';
|
import 'package:pshared/provider/resource.dart';
|
||||||
import 'package:pshared/service/payment/wallets.dart';
|
import 'package:pshared/service/payment/wallets.dart';
|
||||||
@@ -159,6 +161,30 @@ class WalletsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> create({
|
||||||
|
required Describable describable,
|
||||||
|
required ChainAsset asset,
|
||||||
|
required String? ownerRef,
|
||||||
|
}) async {
|
||||||
|
final org = _organizations;
|
||||||
|
if (org == null || !org.isOrganizationSet) return;
|
||||||
|
|
||||||
|
_applyResource(_resource.copyWith(isLoading: true, error: null), notify: true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _service.create(
|
||||||
|
organizationRef: org.current.id,
|
||||||
|
describable: describable,
|
||||||
|
asset: asset,
|
||||||
|
ownerRef: ownerRef,
|
||||||
|
);
|
||||||
|
await loadWalletsWithBalances();
|
||||||
|
} catch (e) {
|
||||||
|
_applyResource(_resource.copyWith(isLoading: false, error: toException(e)), notify: true);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- internals ----------
|
// ---------- internals ----------
|
||||||
|
|
||||||
void _applyResource(Resource<List<Wallet>> newResource, {required bool notify}) {
|
void _applyResource(Resource<List<Wallet>> newResource, {required bool notify}) {
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
|
import 'package:pshared/api/requests/ledger/create.dart';
|
||||||
import 'package:pshared/api/responses/ledger/accounts.dart';
|
import 'package:pshared/api/responses/ledger/accounts.dart';
|
||||||
import 'package:pshared/api/responses/ledger/balance.dart';
|
import 'package:pshared/api/responses/ledger/balance.dart';
|
||||||
|
import 'package:pshared/data/mapper/describable.dart';
|
||||||
import 'package:pshared/data/mapper/ledger/account.dart';
|
import 'package:pshared/data/mapper/ledger/account.dart';
|
||||||
import 'package:pshared/data/mapper/ledger/balance.dart';
|
import 'package:pshared/data/mapper/ledger/balance.dart';
|
||||||
|
import 'package:pshared/data/mapper/ledger/type.dart';
|
||||||
|
import 'package:pshared/models/currency.dart';
|
||||||
|
import 'package:pshared/models/describable.dart';
|
||||||
import 'package:pshared/models/ledger/account.dart';
|
import 'package:pshared/models/ledger/account.dart';
|
||||||
import 'package:pshared/models/ledger/balance.dart';
|
import 'package:pshared/models/ledger/balance.dart';
|
||||||
|
import 'package:pshared/models/ledger/type.dart';
|
||||||
import 'package:pshared/service/authorization/service.dart';
|
import 'package:pshared/service/authorization/service.dart';
|
||||||
import 'package:pshared/service/services.dart';
|
import 'package:pshared/service/services.dart';
|
||||||
|
import 'package:pshared/utils/currency.dart';
|
||||||
|
|
||||||
|
|
||||||
class LedgerService {
|
class LedgerService {
|
||||||
@@ -29,4 +36,22 @@ class LedgerService {
|
|||||||
);
|
);
|
||||||
return LedgerBalanceResponse.fromJson(json).balance.toDomain();
|
return LedgerBalanceResponse.fromJson(json).balance.toDomain();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> create({
|
||||||
|
required String organizationRef,
|
||||||
|
required Describable describable,
|
||||||
|
required String? ownerRef,
|
||||||
|
required Currency currency,
|
||||||
|
}) async => AuthorizationService.getPOSTResponse(
|
||||||
|
_objectType,
|
||||||
|
'/$organizationRef',
|
||||||
|
CreateLedgerAccountRequest(
|
||||||
|
describable: describable.toDTO(),
|
||||||
|
ownerRef: ownerRef,
|
||||||
|
allowNegative: false,
|
||||||
|
isSettlement: false,
|
||||||
|
accountType: LedgerAccountType.asset.toDTO(),
|
||||||
|
currency: currencyCodeToString(currency),
|
||||||
|
).toJson(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
import 'package:pshared/data/mapper/wallet/ui.dart';
|
import 'package:pshared/data/mapper/wallet/ui.dart';
|
||||||
|
import 'package:pshared/models/describable.dart';
|
||||||
import 'package:pshared/models/payment/wallet.dart';
|
import 'package:pshared/models/payment/wallet.dart';
|
||||||
|
import 'package:pshared/models/wallet/chain_asset.dart';
|
||||||
import 'package:pshared/service/wallet.dart' as shared_wallet_service;
|
import 'package:pshared/service/wallet.dart' as shared_wallet_service;
|
||||||
|
|
||||||
|
|
||||||
abstract class WalletsService {
|
abstract class WalletsService {
|
||||||
Future<List<Wallet>> getWallets(String organizationRef);
|
Future<List<Wallet>> getWallets(String organizationRef);
|
||||||
Future<double> getBalance(String organizationRef, String walletRef);
|
Future<double> getBalance(String organizationRef, String walletRef);
|
||||||
|
Future<void> create({
|
||||||
|
required String organizationRef,
|
||||||
|
required Describable describable,
|
||||||
|
required ChainAsset asset,
|
||||||
|
required String? ownerRef,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class ApiWalletsService implements WalletsService {
|
class ApiWalletsService implements WalletsService {
|
||||||
@@ -24,4 +32,17 @@ class ApiWalletsService implements WalletsService {
|
|||||||
final amount = balance.available?.amount;
|
final amount = balance.available?.amount;
|
||||||
return amount == null ? 0 : double.tryParse(amount) ?? 0;
|
return amount == null ? 0 : double.tryParse(amount) ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> create({
|
||||||
|
required String organizationRef,
|
||||||
|
required Describable describable,
|
||||||
|
required ChainAsset asset,
|
||||||
|
required String? ownerRef,
|
||||||
|
}) => shared_wallet_service.WalletService.create(
|
||||||
|
organizationRef: organizationRef,
|
||||||
|
describable: describable,
|
||||||
|
asset: asset,
|
||||||
|
ownerRef: ownerRef,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
|
import 'package:pshared/api/requests/wallet/create.dart';
|
||||||
import 'package:pshared/api/responses/wallet_balance.dart';
|
import 'package:pshared/api/responses/wallet_balance.dart';
|
||||||
import 'package:pshared/api/responses/wallets.dart';
|
import 'package:pshared/api/responses/wallets.dart';
|
||||||
|
import 'package:pshared/data/mapper/describable.dart';
|
||||||
|
import 'package:pshared/data/mapper/wallet/chain_asset.dart';
|
||||||
import 'package:pshared/data/mapper/wallet/response.dart';
|
import 'package:pshared/data/mapper/wallet/response.dart';
|
||||||
|
import 'package:pshared/models/describable.dart';
|
||||||
import 'package:pshared/models/wallet/balance.dart';
|
import 'package:pshared/models/wallet/balance.dart';
|
||||||
|
import 'package:pshared/models/wallet/chain_asset.dart';
|
||||||
import 'package:pshared/models/wallet/wallet.dart';
|
import 'package:pshared/models/wallet/wallet.dart';
|
||||||
import 'package:pshared/service/authorization/service.dart';
|
import 'package:pshared/service/authorization/service.dart';
|
||||||
import 'package:pshared/service/services.dart';
|
import 'package:pshared/service/services.dart';
|
||||||
@@ -28,4 +33,19 @@ class WalletService {
|
|||||||
);
|
);
|
||||||
return WalletBalanceResponse.fromJson(json).toDomain();
|
return WalletBalanceResponse.fromJson(json).toDomain();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<void> create({
|
||||||
|
required String organizationRef,
|
||||||
|
required Describable describable,
|
||||||
|
required ChainAsset asset,
|
||||||
|
required String? ownerRef,
|
||||||
|
}) async => AuthorizationService.getPOSTResponse(
|
||||||
|
_objectType,
|
||||||
|
'/$organizationRef',
|
||||||
|
CreateWalletRequest(
|
||||||
|
asset: asset.toDTO(),
|
||||||
|
describable: describable.toDTO(),
|
||||||
|
ownerRef: ownerRef,
|
||||||
|
).toJson(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -279,7 +279,7 @@ void _openWalletEdit(
|
|||||||
Wallet wallet, {
|
Wallet wallet, {
|
||||||
required PayoutDestination returnTo,
|
required PayoutDestination returnTo,
|
||||||
}) {
|
}) {
|
||||||
context.read<WalletsController>().selectWallet(wallet);
|
context.read<WalletsController>().selectWallet(wallet, allowHidden: true);
|
||||||
context.pushToEditWallet(returnTo: returnTo);
|
context.pushToEditWallet(returnTo: returnTo);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,7 +288,7 @@ void _openWalletTopUp(
|
|||||||
Wallet wallet, {
|
Wallet wallet, {
|
||||||
required PayoutDestination returnTo,
|
required PayoutDestination returnTo,
|
||||||
}) {
|
}) {
|
||||||
context.read<WalletsController>().selectWallet(wallet);
|
context.read<WalletsController>().selectWallet(wallet, allowHidden: true);
|
||||||
context.pushToWalletTopUp(returnTo: returnTo);
|
context.pushToWalletTopUp(returnTo: returnTo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -608,6 +608,8 @@
|
|||||||
"noRecipientsFound": "No recipients found for this query.",
|
"noRecipientsFound": "No recipients found for this query.",
|
||||||
"sourceOfFunds": "Source of funds",
|
"sourceOfFunds": "Source of funds",
|
||||||
"walletTopUp": "Top up",
|
"walletTopUp": "Top up",
|
||||||
|
"errorCreateManagedWallet": "Failed to create managed wallet.",
|
||||||
|
"errorCreateLedgerAccount": "Failed to create ledger account.",
|
||||||
"englishLanguage": "English",
|
"englishLanguage": "English",
|
||||||
"russianLanguage": "Russian",
|
"russianLanguage": "Russian",
|
||||||
"germanLanguage": "German"
|
"germanLanguage": "German"
|
||||||
|
|||||||
@@ -609,6 +609,8 @@
|
|||||||
"noRecipientsFound": "Получатели по запросу не найдены.",
|
"noRecipientsFound": "Получатели по запросу не найдены.",
|
||||||
"sourceOfFunds": "Источник средств",
|
"sourceOfFunds": "Источник средств",
|
||||||
"walletTopUp": "Пополнение",
|
"walletTopUp": "Пополнение",
|
||||||
|
"errorCreateManagedWallet": "Не удалось создать управляемый кошелек.",
|
||||||
|
"errorCreateLedgerAccount": "Не удалось создать счет бухгалтерской книги.",
|
||||||
"englishLanguage": "Английский",
|
"englishLanguage": "Английский",
|
||||||
"russianLanguage": "Русский",
|
"russianLanguage": "Русский",
|
||||||
"germanLanguage": "Немецкий"
|
"germanLanguage": "Немецкий"
|
||||||
|
|||||||
@@ -16,8 +16,10 @@ import 'package:pshared/provider/organizations.dart';
|
|||||||
import 'package:pshared/provider/accounts/employees.dart';
|
import 'package:pshared/provider/accounts/employees.dart';
|
||||||
import 'package:pshared/provider/recipient/pmethods.dart';
|
import 'package:pshared/provider/recipient/pmethods.dart';
|
||||||
import 'package:pshared/provider/recipient/provider.dart';
|
import 'package:pshared/provider/recipient/provider.dart';
|
||||||
|
import 'package:pshared/provider/ledger.dart';
|
||||||
import 'package:pshared/provider/payment/wallets.dart';
|
import 'package:pshared/provider/payment/wallets.dart';
|
||||||
import 'package:pshared/provider/invitations.dart';
|
import 'package:pshared/provider/invitations.dart';
|
||||||
|
import 'package:pshared/service/ledger.dart';
|
||||||
import 'package:pshared/service/payment/wallets.dart';
|
import 'package:pshared/service/payment/wallets.dart';
|
||||||
|
|
||||||
import 'package:pweb/app/app.dart';
|
import 'package:pweb/app/app.dart';
|
||||||
@@ -96,6 +98,10 @@ void main() async {
|
|||||||
create: (_) => WalletsProvider(ApiWalletsService()),
|
create: (_) => WalletsProvider(ApiWalletsService()),
|
||||||
update: (context, organizations, provider) => provider!..update(organizations),
|
update: (context, organizations, provider) => provider!..update(organizations),
|
||||||
),
|
),
|
||||||
|
ChangeNotifierProxyProvider<OrganizationsProvider, LedgerAccountsProvider>(
|
||||||
|
create: (_) => LedgerAccountsProvider(LedgerService()),
|
||||||
|
update: (context, organizations, provider) => provider!..update(organizations),
|
||||||
|
),
|
||||||
ChangeNotifierProxyProvider<WalletsProvider, WalletsController>(
|
ChangeNotifierProxyProvider<WalletsProvider, WalletsController>(
|
||||||
create: (_) => WalletsController(),
|
create: (_) => WalletsController(),
|
||||||
update: (_, wallets, controller) => controller!..update(wallets),
|
update: (_, wallets, controller) => controller!..update(wallets),
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:pshared/models/payment/type.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/utils/text_field_styles.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
|
|
||||||
|
class AssetTypeField extends StatelessWidget {
|
||||||
|
final PaymentType value;
|
||||||
|
final ValueChanged<PaymentType?>? onChanged;
|
||||||
|
|
||||||
|
const AssetTypeField({
|
||||||
|
super.key,
|
||||||
|
required this.value,
|
||||||
|
required this.onChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
return DropdownButtonFormField<PaymentType>(
|
||||||
|
initialValue: value,
|
||||||
|
decoration: getInputDecoration(context, l10n.paymentType, true),
|
||||||
|
items: [
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: PaymentType.managedWallet,
|
||||||
|
child: Text(l10n.paymentTypeManagedWallet),
|
||||||
|
),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: PaymentType.ledger,
|
||||||
|
child: Text(l10n.paymentTypeLedger),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged: onChanged,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
|
||||||
|
class DialogCancelButton extends StatelessWidget {
|
||||||
|
final String label;
|
||||||
|
final bool isSaving;
|
||||||
|
final VoidCallback onCancel;
|
||||||
|
|
||||||
|
const DialogCancelButton({
|
||||||
|
super.key,
|
||||||
|
required this.label,
|
||||||
|
required this.isSaving,
|
||||||
|
required this.onCancel,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => TextButton(
|
||||||
|
onPressed: isSaving ? null : onCancel,
|
||||||
|
child: Text(label),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:dotted_border/dotted_border.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/pages/dashboard/buttons/balance/add/dialog.dart';
|
||||||
|
import 'package:pweb/pages/dashboard/buttons/balance/config.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
|
|
||||||
|
class AddBalanceCard extends StatelessWidget {
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
|
const AddBalanceCard({super.key, this.onTap});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final colorScheme = theme.colorScheme;
|
||||||
|
final textTheme = theme.textTheme;
|
||||||
|
final loc = AppLocalizations.of(context)!;
|
||||||
|
|
||||||
|
final borderRadius = BorderRadius.circular(WalletCardConfig.borderRadius);
|
||||||
|
|
||||||
|
final subtitle = '${loc.paymentTypeLedger} / ${loc.paymentTypeManagedWallet}';
|
||||||
|
final effectiveOnTap = onTap ?? () => showAddBalanceDialog(context);
|
||||||
|
|
||||||
|
return ClipRRect(
|
||||||
|
borderRadius: borderRadius,
|
||||||
|
child: DottedBorder(
|
||||||
|
options: RoundedRectDottedBorderOptions(
|
||||||
|
radius: Radius.circular(WalletCardConfig.borderRadius),
|
||||||
|
dashPattern: const [8, 5],
|
||||||
|
strokeWidth: 1.6,
|
||||||
|
color: colorScheme.primary.withAlpha(110),
|
||||||
|
),
|
||||||
|
child: Material(
|
||||||
|
color: colorScheme.primary.withAlpha(14),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: effectiveOnTap,
|
||||||
|
child: SizedBox.expand(
|
||||||
|
child: Padding(
|
||||||
|
padding: WalletCardConfig.contentPadding,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: colorScheme.primary.withAlpha(28),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.add_rounded,
|
||||||
|
size: 28,
|
||||||
|
color: colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
loc.actionAddNew,
|
||||||
|
style: textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
subtitle,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: textTheme.bodySmall?.copyWith(
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import 'package:pshared/models/currency.dart';
|
||||||
|
import 'package:pshared/models/payment/chain_network.dart';
|
||||||
|
|
||||||
|
|
||||||
|
const String orgOwnerRef = '';
|
||||||
|
const Currency managedCurrencyDefault = Currency.usdt;
|
||||||
|
const Currency ledgerCurrencyDefault = Currency.rub;
|
||||||
|
const ChainNetwork managedNetworkDefault = ChainNetwork.tronMainnet;
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/utils/text_field_styles.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
|
|
||||||
|
class DescriptionField extends StatelessWidget {
|
||||||
|
final TextEditingController controller;
|
||||||
|
|
||||||
|
const DescriptionField({
|
||||||
|
super.key,
|
||||||
|
required this.controller,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
return TextFormField(
|
||||||
|
controller: controller,
|
||||||
|
decoration: getInputDecoration(context, '${l10n.comment} (${l10n.optional})', true),
|
||||||
|
style: getTextFieldStyle(context, true),
|
||||||
|
minLines: 2,
|
||||||
|
maxLines: 3,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import 'package:pshared/models/currency.dart';
|
||||||
|
import 'package:pshared/models/describable.dart';
|
||||||
|
import 'package:pshared/models/payment/chain_network.dart';
|
||||||
|
import 'package:pshared/models/payment/type.dart';
|
||||||
|
import 'package:pshared/models/wallet/chain_asset.dart';
|
||||||
|
import 'package:pshared/provider/accounts/employees.dart';
|
||||||
|
import 'package:pshared/provider/ledger.dart';
|
||||||
|
import 'package:pshared/provider/organizations.dart';
|
||||||
|
import 'package:pshared/provider/payment/wallets.dart';
|
||||||
|
import 'package:pshared/utils/currency.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/pages/dashboard/buttons/balance/add/form.dart';
|
||||||
|
import 'package:pweb/pages/dashboard/buttons/balance/add/constants.dart';
|
||||||
|
import 'package:pweb/pages/dashboard/buttons/balance/add/cancel.dart';
|
||||||
|
import 'package:pweb/pages/dashboard/buttons/balance/add/submit.dart';
|
||||||
|
import 'package:pweb/utils/error/snackbar.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
|
|
||||||
|
Future<void> showAddBalanceDialog(BuildContext context) => showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (dialogContext) => const AddBalanceDialog(),
|
||||||
|
);
|
||||||
|
|
||||||
|
class AddBalanceDialog extends StatefulWidget {
|
||||||
|
const AddBalanceDialog({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AddBalanceDialog> createState() => _AddBalanceDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AddBalanceDialogState extends State<AddBalanceDialog> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
final _nameController = TextEditingController();
|
||||||
|
final _descriptionController = TextEditingController();
|
||||||
|
|
||||||
|
PaymentType _assetType = PaymentType.managedWallet;
|
||||||
|
String _ownerRef = orgOwnerRef;
|
||||||
|
Currency _managedCurrency = managedCurrencyDefault;
|
||||||
|
ChainNetwork _network = managedNetworkDefault;
|
||||||
|
Currency _ledgerCurrency = ledgerCurrencyDefault;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_nameController.dispose();
|
||||||
|
_descriptionController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setAssetType(PaymentType? value) {
|
||||||
|
if (value == null) return;
|
||||||
|
setState(() => _assetType = value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setOwnerRef(String? value) {
|
||||||
|
if (value == null) return;
|
||||||
|
setState(() => _ownerRef = value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setManagedCurrency(Currency? value) {
|
||||||
|
if (value == null) return;
|
||||||
|
setState(() => _managedCurrency = value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setNetwork(ChainNetwork? value) {
|
||||||
|
if (value == null) return;
|
||||||
|
setState(() => _network = value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setLedgerCurrency(Currency? value) {
|
||||||
|
if (value == null) return;
|
||||||
|
setState(() => _ledgerCurrency = value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _submit() async {
|
||||||
|
final form = _formKey.currentState;
|
||||||
|
if (form == null || !form.validate()) return;
|
||||||
|
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final name = _nameController.text.trim();
|
||||||
|
final description = _descriptionController.text.trim();
|
||||||
|
final employees = context.read<EmployeesProvider>().employees;
|
||||||
|
final effectiveOwnerRef = employees.any((employee) => employee.id == _ownerRef) ? _ownerRef : orgOwnerRef;
|
||||||
|
final isOrgWallet = effectiveOwnerRef == orgOwnerRef;
|
||||||
|
final owner = isOrgWallet ? null : employees.firstWhereOrNull((employee) => employee.id == effectiveOwnerRef);
|
||||||
|
|
||||||
|
final errorMessage = _assetType == PaymentType.managedWallet
|
||||||
|
? l10n.errorCreateManagedWallet
|
||||||
|
: l10n.errorCreateLedgerAccount;
|
||||||
|
|
||||||
|
final result = await executeActionWithNotification<bool>(
|
||||||
|
context: context,
|
||||||
|
errorMessage: errorMessage,
|
||||||
|
action: () async {
|
||||||
|
if (_assetType == PaymentType.managedWallet) {
|
||||||
|
await context.read<WalletsProvider>().create(
|
||||||
|
describable: newDescribable(name: name, description: description),
|
||||||
|
asset: ChainAsset(chain: _network, tokenSymbol: currencyCodeToString(_managedCurrency)),
|
||||||
|
ownerRef: owner?.id,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await context.read<LedgerAccountsProvider>().create(
|
||||||
|
describable: newDescribable(name: name, description: description),
|
||||||
|
currency: _ledgerCurrency,
|
||||||
|
ownerRef: owner?.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result == true && mounted) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
final orgName = context.select<OrganizationsProvider, String?>(
|
||||||
|
(provider) => provider.isOrganizationSet ? provider.current.name : null,
|
||||||
|
);
|
||||||
|
final employeesProvider = context.watch<EmployeesProvider>();
|
||||||
|
final employees = employeesProvider.employees;
|
||||||
|
final isSaving = context.watch<WalletsProvider>().isLoading ||
|
||||||
|
context.watch<LedgerAccountsProvider>().isLoading;
|
||||||
|
|
||||||
|
final ownerItems = <DropdownMenuItem<String>>[
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: orgOwnerRef,
|
||||||
|
child: Text(orgName ?? l10n.companyName),
|
||||||
|
),
|
||||||
|
...employees.map((employee) => DropdownMenuItem(
|
||||||
|
value: employee.id,
|
||||||
|
child: Text(employee.fullName.isNotEmpty ? employee.fullName : employee.login),
|
||||||
|
)),
|
||||||
|
];
|
||||||
|
|
||||||
|
final resolvedOwnerRef = ownerItems.any((item) => item.value == _ownerRef)
|
||||||
|
? _ownerRef
|
||||||
|
: orgOwnerRef;
|
||||||
|
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(l10n.actionAddNew),
|
||||||
|
content: AddBalanceForm(
|
||||||
|
formKey: _formKey,
|
||||||
|
assetType: _assetType,
|
||||||
|
isSaving: isSaving,
|
||||||
|
ownerItems: ownerItems,
|
||||||
|
ownerValue: resolvedOwnerRef,
|
||||||
|
onAssetTypeChanged: _setAssetType,
|
||||||
|
onOwnerChanged: _setOwnerRef,
|
||||||
|
nameController: _nameController,
|
||||||
|
descriptionController: _descriptionController,
|
||||||
|
managedCurrency: _managedCurrency,
|
||||||
|
network: _network,
|
||||||
|
ledgerCurrency: _ledgerCurrency,
|
||||||
|
onManagedCurrencyChanged: _setManagedCurrency,
|
||||||
|
onNetworkChanged: _setNetwork,
|
||||||
|
onLedgerCurrencyChanged: _setLedgerCurrency,
|
||||||
|
showEmployeesLoading: employeesProvider.isLoading,
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
DialogCancelButton(
|
||||||
|
label: l10n.cancel,
|
||||||
|
isSaving: isSaving,
|
||||||
|
onCancel: () => Navigator.of(context).pop(),
|
||||||
|
),
|
||||||
|
DialogSubmitButton(
|
||||||
|
label: l10n.add,
|
||||||
|
isSaving: isSaving,
|
||||||
|
onSubmit: _submit,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
|
||||||
|
class EmployeesLoadingIndicator extends StatelessWidget {
|
||||||
|
const EmployeesLoadingIndicator({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => const Padding(
|
||||||
|
padding: EdgeInsets.only(top: 4),
|
||||||
|
child: LinearProgressIndicator(minHeight: 2),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:pshared/models/currency.dart';
|
||||||
|
import 'package:pshared/models/payment/chain_network.dart';
|
||||||
|
import 'package:pshared/models/payment/type.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/pages/dashboard/buttons/balance/add/asset_type_field.dart';
|
||||||
|
import 'package:pweb/pages/dashboard/buttons/balance/add/description.dart';
|
||||||
|
import 'package:pweb/pages/dashboard/buttons/balance/add/employees_loading_indicator.dart';
|
||||||
|
import 'package:pweb/pages/dashboard/buttons/balance/add/ledger_fields.dart';
|
||||||
|
import 'package:pweb/pages/dashboard/buttons/balance/add/managed_wallet_fields.dart';
|
||||||
|
import 'package:pweb/pages/dashboard/buttons/balance/add/name.dart';
|
||||||
|
import 'package:pweb/pages/dashboard/buttons/balance/add/owner.dart';
|
||||||
|
|
||||||
|
|
||||||
|
class AddBalanceForm extends StatelessWidget {
|
||||||
|
final GlobalKey<FormState> formKey;
|
||||||
|
final PaymentType assetType;
|
||||||
|
final bool isSaving;
|
||||||
|
final List<DropdownMenuItem<String>> ownerItems;
|
||||||
|
final String ownerValue;
|
||||||
|
final ValueChanged<PaymentType?> onAssetTypeChanged;
|
||||||
|
final ValueChanged<String?> onOwnerChanged;
|
||||||
|
final TextEditingController nameController;
|
||||||
|
final TextEditingController descriptionController;
|
||||||
|
final Currency managedCurrency;
|
||||||
|
final ChainNetwork network;
|
||||||
|
final Currency ledgerCurrency;
|
||||||
|
final ValueChanged<Currency?> onManagedCurrencyChanged;
|
||||||
|
final ValueChanged<ChainNetwork?> onNetworkChanged;
|
||||||
|
final ValueChanged<Currency?> onLedgerCurrencyChanged;
|
||||||
|
final bool showEmployeesLoading;
|
||||||
|
|
||||||
|
const AddBalanceForm({
|
||||||
|
super.key,
|
||||||
|
required this.formKey,
|
||||||
|
required this.assetType,
|
||||||
|
required this.isSaving,
|
||||||
|
required this.ownerItems,
|
||||||
|
required this.ownerValue,
|
||||||
|
required this.onAssetTypeChanged,
|
||||||
|
required this.onOwnerChanged,
|
||||||
|
required this.nameController,
|
||||||
|
required this.descriptionController,
|
||||||
|
required this.managedCurrency,
|
||||||
|
required this.network,
|
||||||
|
required this.ledgerCurrency,
|
||||||
|
required this.onManagedCurrencyChanged,
|
||||||
|
required this.onNetworkChanged,
|
||||||
|
required this.onLedgerCurrencyChanged,
|
||||||
|
required this.showEmployeesLoading,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => Form(
|
||||||
|
key: formKey,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
spacing: 12,
|
||||||
|
children: [
|
||||||
|
AssetTypeField(
|
||||||
|
value: assetType,
|
||||||
|
onChanged: isSaving ? null : onAssetTypeChanged,
|
||||||
|
),
|
||||||
|
OwnerField(
|
||||||
|
value: ownerValue,
|
||||||
|
items: ownerItems,
|
||||||
|
onChanged: isSaving ? null : onOwnerChanged,
|
||||||
|
),
|
||||||
|
NameField(controller: nameController),
|
||||||
|
DescriptionField(controller: descriptionController),
|
||||||
|
if (assetType == PaymentType.managedWallet)
|
||||||
|
ManagedWalletFields(
|
||||||
|
currency: managedCurrency,
|
||||||
|
network: network,
|
||||||
|
onCurrencyChanged: isSaving ? null : onManagedCurrencyChanged,
|
||||||
|
onNetworkChanged: isSaving ? null : onNetworkChanged,
|
||||||
|
)
|
||||||
|
else
|
||||||
|
LedgerFields(
|
||||||
|
currency: ledgerCurrency,
|
||||||
|
onCurrencyChanged: isSaving ? null : onLedgerCurrencyChanged,
|
||||||
|
),
|
||||||
|
if (showEmployeesLoading) const EmployeesLoadingIndicator(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:pshared/models/currency.dart';
|
||||||
|
import 'package:pshared/utils/currency.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/pages/dashboard/buttons/balance/add/constants.dart';
|
||||||
|
import 'package:pweb/utils/text_field_styles.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
|
|
||||||
|
class LedgerFields extends StatelessWidget {
|
||||||
|
final Currency currency;
|
||||||
|
final ValueChanged<Currency?>? onCurrencyChanged;
|
||||||
|
|
||||||
|
const LedgerFields({
|
||||||
|
super.key,
|
||||||
|
required this.currency,
|
||||||
|
required this.onCurrencyChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => DropdownButtonFormField<Currency>(
|
||||||
|
initialValue: currency,
|
||||||
|
decoration: getInputDecoration(context, AppLocalizations.of(context)!.currency, true),
|
||||||
|
items: [
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: ledgerCurrencyDefault,
|
||||||
|
child: Text(currencyCodeToString(ledgerCurrencyDefault)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged: onCurrencyChanged,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:pshared/models/currency.dart';
|
||||||
|
import 'package:pshared/models/payment/chain_network.dart';
|
||||||
|
import 'package:pshared/utils/currency.dart';
|
||||||
|
import 'package:pshared/utils/l10n/chain.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/pages/dashboard/buttons/balance/add/constants.dart';
|
||||||
|
import 'package:pweb/utils/text_field_styles.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
|
|
||||||
|
class ManagedWalletFields extends StatelessWidget {
|
||||||
|
final Currency currency;
|
||||||
|
final ChainNetwork network;
|
||||||
|
final ValueChanged<Currency?>? onCurrencyChanged;
|
||||||
|
final ValueChanged<ChainNetwork?>? onNetworkChanged;
|
||||||
|
|
||||||
|
const ManagedWalletFields({
|
||||||
|
super.key,
|
||||||
|
required this.currency,
|
||||||
|
required this.network,
|
||||||
|
required this.onCurrencyChanged,
|
||||||
|
required this.onNetworkChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
return Column(
|
||||||
|
spacing: 12,
|
||||||
|
children: [
|
||||||
|
DropdownButtonFormField<Currency>(
|
||||||
|
initialValue: currency,
|
||||||
|
decoration: getInputDecoration(context, l10n.currency, true),
|
||||||
|
items: [
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: managedCurrencyDefault,
|
||||||
|
child: Text(currencyCodeToString(managedCurrencyDefault)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged: onCurrencyChanged,
|
||||||
|
),
|
||||||
|
DropdownButtonFormField<ChainNetwork>(
|
||||||
|
initialValue: network,
|
||||||
|
decoration: getInputDecoration(context, l10n.walletTopUpNetworkLabel, true),
|
||||||
|
items: [
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: managedNetworkDefault,
|
||||||
|
child: Text(managedNetworkDefault.localizedName(context)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
onChanged: onNetworkChanged,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/utils/text_field_styles.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
|
|
||||||
|
class NameField extends StatelessWidget {
|
||||||
|
final TextEditingController controller;
|
||||||
|
|
||||||
|
const NameField({
|
||||||
|
super.key,
|
||||||
|
required this.controller,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => TextFormField(
|
||||||
|
controller: controller,
|
||||||
|
decoration: getInputDecoration(context, AppLocalizations.of(context)!.accountName, true),
|
||||||
|
style: getTextFieldStyle(context, true),
|
||||||
|
validator: (value) => (value == null || value.trim().isEmpty) ? AppLocalizations.of(context)!.errorNameMissing : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/utils/text_field_styles.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
|
|
||||||
|
class OwnerField extends StatelessWidget {
|
||||||
|
final String value;
|
||||||
|
final List<DropdownMenuItem<String>> items;
|
||||||
|
final ValueChanged<String?>? onChanged;
|
||||||
|
|
||||||
|
const OwnerField({
|
||||||
|
super.key,
|
||||||
|
required this.value,
|
||||||
|
required this.items,
|
||||||
|
required this.onChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => DropdownButtonFormField<String>(
|
||||||
|
initialValue: value,
|
||||||
|
decoration: getInputDecoration(context, AppLocalizations.of(context)!.colDataOwner, true),
|
||||||
|
items: items,
|
||||||
|
onChanged: onChanged,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
|
||||||
|
class DialogSubmitButton extends StatelessWidget {
|
||||||
|
final String label;
|
||||||
|
final bool isSaving;
|
||||||
|
final VoidCallback onSubmit;
|
||||||
|
|
||||||
|
const DialogSubmitButton({
|
||||||
|
super.key,
|
||||||
|
required this.label,
|
||||||
|
required this.isSaving,
|
||||||
|
required this.onSubmit,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => ElevatedButton(
|
||||||
|
onPressed: isSaving ? null : onSubmit,
|
||||||
|
child: isSaving
|
||||||
|
? const SizedBox(
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: Text(label),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,10 +3,12 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import 'package:pshared/controllers/wallets.dart';
|
import 'package:pshared/controllers/wallets.dart';
|
||||||
|
import 'package:pshared/provider/ledger.dart';
|
||||||
import 'package:pshared/models/payment/wallet.dart';
|
import 'package:pshared/models/payment/wallet.dart';
|
||||||
|
|
||||||
import 'package:pweb/pages/dashboard/buttons/balance/carousel.dart';
|
import 'package:pweb/pages/dashboard/buttons/balance/carousel.dart';
|
||||||
import 'package:pweb/pages/dashboard/buttons/balance/controller.dart';
|
import 'package:pweb/pages/dashboard/buttons/balance/controller.dart';
|
||||||
|
import 'package:pweb/pages/dashboard/buttons/balance/balance_item.dart';
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
@@ -22,38 +24,69 @@ class BalanceWidget extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final walletsController = context.watch<WalletsController>();
|
final walletsController = context.watch<WalletsController>();
|
||||||
|
final ledgerProvider = context.watch<LedgerAccountsProvider>();
|
||||||
final carousel = context.watch<CarouselIndexController>();
|
final carousel = context.watch<CarouselIndexController>();
|
||||||
final loc = AppLocalizations.of(context)!;
|
final loc = AppLocalizations.of(context)!;
|
||||||
|
|
||||||
if (walletsController.isLoading) {
|
final wallets = walletsController.wallets;
|
||||||
|
final accounts = ledgerProvider.accounts;
|
||||||
|
final isLoading = walletsController.isLoading &&
|
||||||
|
ledgerProvider.isLoading &&
|
||||||
|
wallets.isEmpty &&
|
||||||
|
accounts.isEmpty;
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
|
|
||||||
final wallets = walletsController.wallets;
|
final items = [
|
||||||
|
...wallets.map(BalanceItem.wallet),
|
||||||
|
...accounts.map(BalanceItem.ledger),
|
||||||
|
const BalanceItem.addAction(),
|
||||||
|
];
|
||||||
|
|
||||||
if (wallets.isEmpty) {
|
if (items.isEmpty) {
|
||||||
return Center(child: Text(loc.noWalletsAvailable));
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure index is always valid when wallets list changes
|
// Ensure index is always valid when list changes
|
||||||
carousel.setIndex(carousel.index, wallets.length);
|
carousel.setIndex(carousel.index, items.length);
|
||||||
|
|
||||||
final index = carousel.index;
|
final index = carousel.index;
|
||||||
final wallet = wallets[index];
|
final current = items[index];
|
||||||
|
|
||||||
// Single source of truth: controller
|
// Single source of truth: controller
|
||||||
|
if (current.isWallet) {
|
||||||
|
final wallet = current.wallet!;
|
||||||
if (walletsController.selectedWallet?.id != wallet.id) {
|
if (walletsController.selectedWallet?.id != wallet.id) {
|
||||||
walletsController.selectWallet(wallet);
|
walletsController.selectWallet(wallet);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return WalletCarousel(
|
final carouselWidget = BalanceCarousel(
|
||||||
wallets: wallets,
|
items: items,
|
||||||
currentIndex: index,
|
currentIndex: index,
|
||||||
onIndexChanged: (i) {
|
onIndexChanged: (i) {
|
||||||
carousel.setIndex(i, wallets.length);
|
carousel.setIndex(i, items.length);
|
||||||
walletsController.selectWallet(wallets[i]);
|
final next = items[carousel.index];
|
||||||
|
if (next.isWallet) {
|
||||||
|
walletsController.selectWallet(next.wallet!);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onTopUp: onTopUp,
|
onTopUp: onTopUp,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (wallets.isEmpty && accounts.isEmpty) {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Center(child: Text(loc.noWalletsAvailable)),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
carouselWidget,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return carouselWidget;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import 'package:pshared/models/ledger/account.dart';
|
||||||
|
import 'package:pshared/models/payment/wallet.dart';
|
||||||
|
|
||||||
|
|
||||||
|
enum BalanceItemType { wallet, ledger, addAction }
|
||||||
|
|
||||||
|
class BalanceItem {
|
||||||
|
final BalanceItemType type;
|
||||||
|
final Wallet? wallet;
|
||||||
|
final LedgerAccount? account;
|
||||||
|
|
||||||
|
const BalanceItem.wallet(this.wallet) : type = BalanceItemType.wallet, account = null;
|
||||||
|
|
||||||
|
const BalanceItem.ledger(this.account) : type = BalanceItemType.ledger, wallet = null;
|
||||||
|
|
||||||
|
const BalanceItem.addAction() : type = BalanceItemType.addAction, wallet = null, account = null;
|
||||||
|
|
||||||
|
bool get isWallet => type == BalanceItemType.wallet;
|
||||||
|
bool get isLedger => type == BalanceItemType.ledger;
|
||||||
|
bool get isAdd => type == BalanceItemType.addAction;
|
||||||
|
}
|
||||||
@@ -4,12 +4,16 @@ import 'package:provider/provider.dart';
|
|||||||
|
|
||||||
import 'package:pshared/controllers/wallets.dart';
|
import 'package:pshared/controllers/wallets.dart';
|
||||||
import 'package:pshared/models/payment/wallet.dart';
|
import 'package:pshared/models/payment/wallet.dart';
|
||||||
|
import 'package:pshared/models/payment/chain_network.dart';
|
||||||
|
import 'package:pshared/utils/l10n/chain.dart';
|
||||||
|
|
||||||
import 'package:pweb/pages/dashboard/buttons/balance/add_funds.dart';
|
import 'package:pweb/pages/dashboard/buttons/balance/add_funds.dart';
|
||||||
import 'package:pweb/pages/dashboard/buttons/balance/amount.dart';
|
import 'package:pweb/pages/dashboard/buttons/balance/amount.dart';
|
||||||
import 'package:pweb/pages/dashboard/buttons/balance/config.dart';
|
import 'package:pweb/pages/dashboard/buttons/balance/config.dart';
|
||||||
import 'package:pweb/pages/dashboard/buttons/balance/header.dart';
|
import 'package:pweb/pages/dashboard/buttons/balance/header.dart';
|
||||||
import 'package:pweb/widgets/wallet_balance_refresh_button.dart';
|
import 'package:pweb/widgets/refresh_balance/wallet.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
|
|
||||||
class WalletCard extends StatelessWidget {
|
class WalletCard extends StatelessWidget {
|
||||||
@@ -24,20 +28,29 @@ class WalletCard extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final loc = AppLocalizations.of(context)!;
|
||||||
|
final networkLabel = (wallet.network == null || wallet.network == ChainNetwork.unspecified)
|
||||||
|
? null
|
||||||
|
: wallet.network!.localizedName(context);
|
||||||
|
final symbol = wallet.tokenSymbol?.trim();
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
color: Theme.of(context).colorScheme.onSecondary,
|
color: Theme.of(context).colorScheme.onSecondary,
|
||||||
elevation: WalletCardConfig.elevation,
|
elevation: WalletCardConfig.elevation,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(WalletCardConfig.borderRadius),
|
borderRadius: BorderRadius.circular(WalletCardConfig.borderRadius),
|
||||||
),
|
),
|
||||||
|
child: SizedBox.expand(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: WalletCardConfig.contentPadding,
|
padding: WalletCardConfig.contentPadding,
|
||||||
child: Column(
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
BalanceHeader(
|
BalanceHeader(
|
||||||
walletNetwork: wallet.network,
|
title: loc.paymentTypeCryptoWallet,
|
||||||
tokenSymbol: wallet.tokenSymbol,
|
subtitle: networkLabel,
|
||||||
|
badge: (symbol == null || symbol.isEmpty) ? null : symbol,
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -48,18 +61,16 @@ class WalletCard extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
WalletBalanceRefreshButton(
|
WalletBalanceRefreshButton(
|
||||||
walletId: wallet.id,
|
walletRef: wallet.id,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
BalanceAddFunds(
|
BalanceAddFunds(onTopUp: onTopUp),
|
||||||
onTopUp: () {
|
|
||||||
onTopUp();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +1,117 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
|
||||||
import 'package:pshared/models/payment/wallet.dart';
|
import 'package:pshared/models/payment/wallet.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/pages/dashboard/buttons/balance/add/card.dart';
|
||||||
|
import 'package:pweb/pages/dashboard/buttons/balance/balance_item.dart';
|
||||||
import 'package:pweb/pages/dashboard/buttons/balance/card.dart';
|
import 'package:pweb/pages/dashboard/buttons/balance/card.dart';
|
||||||
import 'package:pweb/pages/dashboard/buttons/balance/config.dart';
|
import 'package:pweb/pages/dashboard/buttons/balance/config.dart';
|
||||||
import 'package:pweb/pages/dashboard/buttons/balance/indicator.dart';
|
import 'package:pweb/pages/dashboard/buttons/balance/indicator.dart';
|
||||||
|
import 'package:pweb/pages/dashboard/buttons/balance/ledger.dart';
|
||||||
|
|
||||||
|
|
||||||
class WalletCarousel extends StatelessWidget {
|
class BalanceCarousel extends StatefulWidget {
|
||||||
final List<Wallet> wallets;
|
final List<BalanceItem> items;
|
||||||
final int currentIndex;
|
final int currentIndex;
|
||||||
final ValueChanged<int> onIndexChanged;
|
final ValueChanged<int> onIndexChanged;
|
||||||
final ValueChanged<Wallet> onTopUp;
|
final ValueChanged<Wallet> onTopUp;
|
||||||
|
|
||||||
const WalletCarousel({
|
const BalanceCarousel({
|
||||||
super.key,
|
super.key,
|
||||||
required this.wallets,
|
required this.items,
|
||||||
required this.currentIndex,
|
required this.currentIndex,
|
||||||
required this.onIndexChanged,
|
required this.onIndexChanged,
|
||||||
required this.onTopUp,
|
required this.onTopUp,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<BalanceCarousel> createState() => _BalanceCarouselState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BalanceCarouselState extends State<BalanceCarousel> {
|
||||||
|
late final PageController _controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = PageController(
|
||||||
|
initialPage: widget.currentIndex,
|
||||||
|
viewportFraction: WalletCardConfig.viewportFraction,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant BalanceCarousel oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (!mounted) return;
|
||||||
|
if (_controller.hasClients) {
|
||||||
|
final currentPage = _controller.page?.round();
|
||||||
|
if (currentPage != widget.currentIndex) {
|
||||||
|
_controller.jumpToPage(widget.currentIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _goToPage(int index) {
|
||||||
|
if (!_controller.hasClients) return;
|
||||||
|
_controller.animateToPage(
|
||||||
|
index,
|
||||||
|
duration: const Duration(milliseconds: 220),
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (wallets.isEmpty) {
|
if (widget.items.isEmpty) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
final safeIndex = currentIndex.clamp(0, wallets.length - 1);
|
final safeIndex = widget.currentIndex.clamp(0, widget.items.length - 1);
|
||||||
final wallet = wallets[safeIndex];
|
final scrollBehavior = ScrollConfiguration.of(context).copyWith(
|
||||||
|
dragDevices: const {
|
||||||
|
PointerDeviceKind.touch,
|
||||||
|
PointerDeviceKind.mouse,
|
||||||
|
PointerDeviceKind.trackpad,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: WalletCardConfig.cardHeight,
|
height: WalletCardConfig.cardHeight,
|
||||||
child: Padding(
|
child: MouseRegion(
|
||||||
|
cursor: SystemMouseCursors.grab,
|
||||||
|
child: ScrollConfiguration(
|
||||||
|
behavior: scrollBehavior,
|
||||||
|
child: PageView.builder(
|
||||||
|
controller: _controller,
|
||||||
|
onPageChanged: widget.onIndexChanged,
|
||||||
|
itemCount: widget.items.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final item = widget.items[index];
|
||||||
|
final Widget card = switch (item.type) {
|
||||||
|
BalanceItemType.wallet => WalletCard(
|
||||||
|
wallet: item.wallet!,
|
||||||
|
onTopUp: () => widget.onTopUp(item.wallet!),
|
||||||
|
),
|
||||||
|
BalanceItemType.ledger => LedgerAccountCard(account: item.account!),
|
||||||
|
BalanceItemType.addAction => const AddBalanceCard(),
|
||||||
|
};
|
||||||
|
|
||||||
|
return Padding(
|
||||||
padding: WalletCardConfig.cardPadding,
|
padding: WalletCardConfig.cardPadding,
|
||||||
child: WalletCard(
|
child: card,
|
||||||
wallet: wallet,
|
);
|
||||||
onTopUp: () => onTopUp(wallet),
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -47,20 +121,18 @@ class WalletCarousel extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: safeIndex > 0
|
onPressed: safeIndex > 0
|
||||||
? () => onIndexChanged(safeIndex - 1)
|
? () => _goToPage(safeIndex - 1)
|
||||||
: null,
|
: null,
|
||||||
icon: const Icon(Icons.arrow_back),
|
icon: const Icon(Icons.arrow_back),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
CarouselIndicator(
|
CarouselIndicator(
|
||||||
itemCount: wallets.length,
|
itemCount: widget.items.length,
|
||||||
index: safeIndex,
|
index: safeIndex,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: safeIndex < wallets.length - 1
|
onPressed: safeIndex < widget.items.length - 1 ? () => _goToPage(safeIndex + 1) : null,
|
||||||
? () => onIndexChanged(safeIndex + 1)
|
|
||||||
: null,
|
|
||||||
icon: const Icon(Icons.arrow_forward),
|
icon: const Icon(Icons.arrow_forward),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,29 +1,24 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:pshared/models/payment/chain_network.dart';
|
|
||||||
import 'package:pshared/utils/l10n/chain.dart';
|
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
|
||||||
|
|
||||||
class BalanceHeader extends StatelessWidget {
|
class BalanceHeader extends StatelessWidget {
|
||||||
final ChainNetwork? walletNetwork;
|
final String title;
|
||||||
final String? tokenSymbol;
|
final String? subtitle;
|
||||||
|
final String? badge;
|
||||||
|
|
||||||
const BalanceHeader({
|
const BalanceHeader({
|
||||||
super.key,
|
super.key,
|
||||||
this.walletNetwork,
|
required this.title,
|
||||||
this.tokenSymbol,
|
this.subtitle,
|
||||||
|
this.badge,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final textTheme = Theme.of(context).textTheme;
|
final textTheme = Theme.of(context).textTheme;
|
||||||
final colorScheme = Theme.of(context).colorScheme;
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
final loc = AppLocalizations.of(context)!;
|
final subtitleText = subtitle?.trim();
|
||||||
final symbol = tokenSymbol?.trim();
|
final badgeText = badge?.trim();
|
||||||
final networkLabel = (walletNetwork == null || walletNetwork == ChainNetwork.unspecified)
|
|
||||||
? null
|
|
||||||
: walletNetwork!.localizedName(context);
|
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -32,14 +27,14 @@ class BalanceHeader extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
loc.paymentTypeCryptoWallet,
|
title,
|
||||||
style: textTheme.titleMedium?.copyWith(
|
style: textTheme.titleMedium?.copyWith(
|
||||||
color: colorScheme.onSurface,
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (networkLabel != null)
|
if (subtitleText != null && subtitleText.isNotEmpty)
|
||||||
Text(
|
Text(
|
||||||
networkLabel,
|
subtitleText,
|
||||||
style: textTheme.bodySmall?.copyWith(
|
style: textTheme.bodySmall?.copyWith(
|
||||||
color: colorScheme.onSurfaceVariant,
|
color: colorScheme.onSurfaceVariant,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
@@ -48,7 +43,7 @@ class BalanceHeader extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (symbol != null && symbol.isNotEmpty) ...[
|
if (badgeText != null && badgeText.isNotEmpty) ...[
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||||
@@ -57,7 +52,7 @@ class BalanceHeader extends StatelessWidget {
|
|||||||
borderRadius: BorderRadius.circular(999),
|
borderRadius: BorderRadius.circular(999),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
symbol,
|
badgeText,
|
||||||
style: textTheme.bodyMedium?.copyWith(
|
style: textTheme.bodyMedium?.copyWith(
|
||||||
color: colorScheme.onPrimaryContainer,
|
color: colorScheme.onPrimaryContainer,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:pshared/models/ledger/account.dart';
|
||||||
|
import 'package:pshared/utils/currency.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
import 'package:pweb/pages/dashboard/buttons/balance/config.dart';
|
||||||
|
import 'package:pweb/pages/dashboard/buttons/balance/header.dart';
|
||||||
|
import 'package:pweb/widgets/refresh_balance/ledger.dart';
|
||||||
|
|
||||||
|
|
||||||
|
class LedgerAccountCard extends StatelessWidget {
|
||||||
|
final LedgerAccount account;
|
||||||
|
|
||||||
|
const LedgerAccountCard({
|
||||||
|
super.key,
|
||||||
|
required this.account,
|
||||||
|
});
|
||||||
|
|
||||||
|
String _formatBalance() {
|
||||||
|
final money = account.balance?.balance;
|
||||||
|
if (money == null) return '--';
|
||||||
|
|
||||||
|
final amount = double.tryParse(money.amount);
|
||||||
|
if (amount == null) {
|
||||||
|
return '${money.amount} ${money.currency}';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final currency = currencyStringToCode(money.currency);
|
||||||
|
final symbol = currencyCodeToSymbol(currency);
|
||||||
|
if (symbol.trim().isEmpty) {
|
||||||
|
return '${amountToString(amount)} ${money.currency}';
|
||||||
|
}
|
||||||
|
return '${amountToString(amount)} $symbol';
|
||||||
|
} catch (_) {
|
||||||
|
return '${amountToString(amount)} ${money.currency}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final textTheme = Theme.of(context).textTheme;
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
final loc = AppLocalizations.of(context)!;
|
||||||
|
final subtitle = account.name.isNotEmpty ? account.name : account.accountCode;
|
||||||
|
final badge = account.currency.trim().isEmpty ? null : account.currency.toUpperCase();
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
color: colorScheme.onSecondary,
|
||||||
|
elevation: WalletCardConfig.elevation,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(WalletCardConfig.borderRadius),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: WalletCardConfig.contentPadding,
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
BalanceHeader(
|
||||||
|
title: loc.paymentTypeLedger,
|
||||||
|
subtitle: subtitle.isNotEmpty ? subtitle : null,
|
||||||
|
badge: badge,
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
_formatBalance(),
|
||||||
|
style: textTheme.headlineSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
LedgerBalanceRefreshButton(
|
||||||
|
ledgerAccountRef: account.ledgerAccountRef,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ import 'package:pweb/pages/payment_methods/widgets/recipient_section.dart';
|
|||||||
import 'package:pweb/pages/payment_methods/widgets/section_title.dart';
|
import 'package:pweb/pages/payment_methods/widgets/section_title.dart';
|
||||||
import 'package:pweb/utils/dimensions.dart';
|
import 'package:pweb/utils/dimensions.dart';
|
||||||
import 'package:pweb/widgets/sidebar/destinations.dart';
|
import 'package:pweb/widgets/sidebar/destinations.dart';
|
||||||
import 'package:pweb/widgets/wallet_balance_refresh_button.dart';
|
import 'package:pweb/widgets/refresh_balance/wallet.dart';
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ class PaymentPageContent extends StatelessWidget {
|
|||||||
if (selectedWalletId == null) {
|
if (selectedWalletId == null) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
return WalletBalanceRefreshButton(walletId: selectedWalletId);
|
return WalletBalanceRefreshButton(walletRef: selectedWalletId);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import 'package:pshared/models/payment/wallet.dart';
|
|||||||
|
|
||||||
import 'package:pweb/models/visibility.dart';
|
import 'package:pweb/models/visibility.dart';
|
||||||
import 'package:pweb/pages/dashboard/buttons/balance/amount.dart';
|
import 'package:pweb/pages/dashboard/buttons/balance/amount.dart';
|
||||||
import 'package:pweb/widgets/wallet_balance_refresh_button.dart';
|
import 'package:pweb/widgets/refresh_balance/wallet.dart';
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ class WalletCard extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
WalletBalanceRefreshButton(
|
WalletBalanceRefreshButton(
|
||||||
walletId: wallet.id,
|
walletRef: wallet.id,
|
||||||
iconOnly: VisibilityState.hidden,
|
iconOnly: VisibilityState.hidden,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:pshared/controllers/wallets.dart';
|
import 'package:pshared/controllers/wallets.dart';
|
||||||
|
|
||||||
import 'package:pweb/pages/dashboard/buttons/balance/amount.dart';
|
import 'package:pweb/pages/dashboard/buttons/balance/amount.dart';
|
||||||
import 'package:pweb/widgets/wallet_balance_refresh_button.dart';
|
import 'package:pweb/widgets/refresh_balance/wallet.dart';
|
||||||
|
|
||||||
|
|
||||||
class WalletEditFields extends StatelessWidget {
|
class WalletEditFields extends StatelessWidget {
|
||||||
@@ -33,7 +33,7 @@ class WalletEditFields extends StatelessWidget {
|
|||||||
onToggleVisibility: () => controller.toggleVisibility(wallet.id),
|
onToggleVisibility: () => controller.toggleVisibility(wallet.id),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
WalletBalanceRefreshButton(walletId: wallet.id),
|
WalletBalanceRefreshButton(walletRef: wallet.id),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|||||||
57
frontend/pweb/lib/widgets/refresh_balance/button.dart
Normal file
57
frontend/pweb/lib/widgets/refresh_balance/button.dart
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/models/visibility.dart';
|
||||||
|
|
||||||
|
|
||||||
|
class BalanceRefreshButton extends StatelessWidget {
|
||||||
|
final bool isBusy;
|
||||||
|
final bool enabled;
|
||||||
|
final VoidCallback onPressed;
|
||||||
|
final VisibilityState iconOnly;
|
||||||
|
final String label;
|
||||||
|
final String tooltip;
|
||||||
|
final double iconSize;
|
||||||
|
|
||||||
|
const BalanceRefreshButton({
|
||||||
|
super.key,
|
||||||
|
required this.isBusy,
|
||||||
|
required this.enabled,
|
||||||
|
required this.onPressed,
|
||||||
|
required this.iconOnly,
|
||||||
|
required this.label,
|
||||||
|
required this.tooltip,
|
||||||
|
this.iconSize = 18,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final canPress = enabled && !isBusy;
|
||||||
|
|
||||||
|
if (iconOnly == VisibilityState.hidden) {
|
||||||
|
return IconButton(
|
||||||
|
tooltip: tooltip,
|
||||||
|
onPressed: canPress ? onPressed : null,
|
||||||
|
icon: isBusy
|
||||||
|
? SizedBox(
|
||||||
|
width: iconSize,
|
||||||
|
height: iconSize,
|
||||||
|
child: const CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.refresh),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return TextButton.icon(
|
||||||
|
onPressed: canPress ? onPressed : null,
|
||||||
|
icon: isBusy
|
||||||
|
? SizedBox(
|
||||||
|
width: iconSize,
|
||||||
|
height: iconSize,
|
||||||
|
child: const CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.refresh),
|
||||||
|
label: Text(label),
|
||||||
|
style: TextButton.styleFrom(visualDensity: VisualDensity.compact),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
46
frontend/pweb/lib/widgets/refresh_balance/ledger.dart
Normal file
46
frontend/pweb/lib/widgets/refresh_balance/ledger.dart
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import 'package:pshared/provider/ledger.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/models/visibility.dart';
|
||||||
|
import 'package:pweb/widgets/refresh_balance/button.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
|
|
||||||
|
class LedgerBalanceRefreshButton extends StatelessWidget {
|
||||||
|
final String ledgerAccountRef;
|
||||||
|
final VisibilityState iconOnly;
|
||||||
|
final double iconSize = 18;
|
||||||
|
|
||||||
|
const LedgerBalanceRefreshButton({
|
||||||
|
super.key,
|
||||||
|
required this.ledgerAccountRef,
|
||||||
|
this.iconOnly = VisibilityState.visible,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final ledgerProvider = context.watch<LedgerAccountsProvider>();
|
||||||
|
final loc = AppLocalizations.of(context)!;
|
||||||
|
final isBusy = ledgerProvider.isWalletRefreshing(ledgerAccountRef) || ledgerProvider.isLoading;
|
||||||
|
final hasTarget = ledgerProvider.accounts.any((a) => a.ledgerAccountRef == ledgerAccountRef);
|
||||||
|
|
||||||
|
void refresh() {
|
||||||
|
final provider = context.read<LedgerAccountsProvider>();
|
||||||
|
provider.refreshBalance(ledgerAccountRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
return BalanceRefreshButton(
|
||||||
|
isBusy: isBusy,
|
||||||
|
enabled: hasTarget,
|
||||||
|
onPressed: refresh,
|
||||||
|
iconOnly: iconOnly,
|
||||||
|
label: loc.refreshBalance,
|
||||||
|
tooltip: loc.refreshBalance,
|
||||||
|
iconSize: iconSize,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
46
frontend/pweb/lib/widgets/refresh_balance/wallet.dart
Normal file
46
frontend/pweb/lib/widgets/refresh_balance/wallet.dart
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import 'package:pshared/provider/payment/wallets.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/models/visibility.dart';
|
||||||
|
import 'package:pweb/widgets/refresh_balance/button.dart';
|
||||||
|
|
||||||
|
import 'package:pweb/generated/i18n/app_localizations.dart';
|
||||||
|
|
||||||
|
|
||||||
|
class WalletBalanceRefreshButton extends StatelessWidget {
|
||||||
|
final String walletRef;
|
||||||
|
final VisibilityState iconOnly;
|
||||||
|
final double iconSize = 18;
|
||||||
|
|
||||||
|
const WalletBalanceRefreshButton({
|
||||||
|
super.key,
|
||||||
|
required this.walletRef,
|
||||||
|
this.iconOnly = VisibilityState.visible,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final walletsProvider = context.watch<WalletsProvider>();
|
||||||
|
final loc = AppLocalizations.of(context)!;
|
||||||
|
final isBusy = walletsProvider.isWalletRefreshing(walletRef) || walletsProvider.isLoading;
|
||||||
|
final hasTarget = walletsProvider.wallets.any((w) => w.id == walletRef);
|
||||||
|
|
||||||
|
void refresh() {
|
||||||
|
final provider = context.read<WalletsProvider>();
|
||||||
|
provider.refreshBalance(walletRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
return BalanceRefreshButton(
|
||||||
|
isBusy: isBusy,
|
||||||
|
enabled: hasTarget,
|
||||||
|
onPressed: refresh,
|
||||||
|
iconOnly: iconOnly,
|
||||||
|
label: loc.refreshBalance,
|
||||||
|
tooltip: loc.refreshBalance,
|
||||||
|
iconSize: iconSize,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
import 'package:pshared/provider/payment/wallets.dart';
|
|
||||||
|
|
||||||
import 'package:pweb/models/visibility.dart';
|
|
||||||
|
|
||||||
import 'package:pweb/generated/i18n/app_localizations.dart';
|
|
||||||
|
|
||||||
|
|
||||||
class WalletBalanceRefreshButton extends StatelessWidget {
|
|
||||||
final String walletId;
|
|
||||||
final VisibilityState iconOnly;
|
|
||||||
final double iconSize = 18;
|
|
||||||
|
|
||||||
const WalletBalanceRefreshButton({
|
|
||||||
super.key,
|
|
||||||
required this.walletId,
|
|
||||||
this.iconOnly = VisibilityState.visible,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final walletsProvider = context.watch<WalletsProvider>();
|
|
||||||
final loc = AppLocalizations.of(context)!;
|
|
||||||
final isBusy = walletsProvider.isWalletRefreshing(walletId) || walletsProvider.isLoading;
|
|
||||||
final hasTarget = walletsProvider.wallets.any((w) => w.id == walletId);
|
|
||||||
|
|
||||||
void refresh() {
|
|
||||||
final provider = context.read<WalletsProvider>();
|
|
||||||
provider.refreshBalance(walletId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (iconOnly == VisibilityState.hidden) {
|
|
||||||
return IconButton(
|
|
||||||
tooltip: loc.refreshBalance,
|
|
||||||
onPressed: hasTarget && !isBusy ? refresh : null,
|
|
||||||
icon: isBusy
|
|
||||||
? SizedBox(
|
|
||||||
width: iconSize,
|
|
||||||
height: iconSize,
|
|
||||||
child: const CircularProgressIndicator(strokeWidth: 2),
|
|
||||||
)
|
|
||||||
: const Icon(Icons.refresh),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return TextButton.icon(
|
|
||||||
onPressed: hasTarget && !isBusy ? refresh : null,
|
|
||||||
icon: isBusy
|
|
||||||
? SizedBox(
|
|
||||||
width: iconSize,
|
|
||||||
height: iconSize,
|
|
||||||
child: const CircularProgressIndicator(strokeWidth: 2),
|
|
||||||
)
|
|
||||||
: const Icon(Icons.refresh),
|
|
||||||
label: Text(loc.refreshBalance),
|
|
||||||
style: TextButton.styleFrom(visualDensity: VisualDensity.compact),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user