diff --git a/.gitignore b/.gitignore index 2c4105f7..2db2c100 100644 --- a/.gitignore +++ b/.gitignore @@ -43,7 +43,6 @@ next-env.d.ts *storybook.log # Storybook -.storybook /storybook-static/ # Playwright diff --git a/.storybook/main.ts b/.storybook/main.ts new file mode 100644 index 00000000..c4ed6878 --- /dev/null +++ b/.storybook/main.ts @@ -0,0 +1,57 @@ +import type { StorybookConfig } from '@storybook/nextjs' + +const config: StorybookConfig = { + stories: [ + '../src/components/**/*.mdx', + '../src/components/**/*.stories.@(js|jsx|mjs|ts|tsx)' + ], + staticDirs: ['../public'], + addons: [ + '@storybook/addon-onboarding', + '@storybook/addon-links', + '@storybook/addon-essentials', + '@chromatic-com/storybook', + '@storybook/addon-interactions', + '@storybook/addon-styling-webpack', + + { + name: '@storybook/addon-styling-webpack', + options: { + rules: [ + { + test: /\.css$/, + sideEffects: true, + use: [ + require.resolve('style-loader'), + { + loader: require.resolve('css-loader'), + options: { + importLoaders: 1 + } + }, + { + loader: require.resolve('postcss-loader'), + options: { + implementation: require.resolve('postcss') + } + } + ] + } + ] + } + } + ], + + framework: { + name: '@storybook/nextjs', + options: {} + }, + typescript: { + reactDocgen: 'react-docgen-typescript' + }, + docs: { + autodocs: 'tag' + }, + build: {} +} +export default config diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx new file mode 100644 index 00000000..d7963faa --- /dev/null +++ b/.storybook/preview.tsx @@ -0,0 +1,34 @@ +import type { Preview } from '@storybook/react' + +import '../src/app/globals.css' + +import React from 'react' +import { ThemeProvider } from '../src/lib/theme/theme-provider' +import { IntlProvider } from 'react-intl' + +const preview: Preview = { + parameters: { + backgrounds: { + values: [{ name: 'Light', value: '#f4f4f5' }], + default: 'Light' + }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i + } + } + } +} + +export const decorators = [ + (Story) => ( + + + + + + ) +] + +export default preview diff --git a/locales/extracted/en.json b/locales/extracted/en.json index ad1f9360..3a8209de 100644 --- a/locales/extracted/en.json +++ b/locales/extracted/en.json @@ -43,6 +43,7 @@ "common.noOptions": "No options found.", "common.portfolio": "Portfolio", "common.previousPage": "Previous page", + "common.records": "records", "common.remove": "Remove", "common.requiredFields": "(*) required fields.", "common.resources": "Resources", @@ -158,7 +159,7 @@ "ledgers.account.sheet.edit.title": "Edit {accountName}", "ledgers.account.sheet.tabs.details": "Account Details", "ledgers.accounts.createFirst": "Create first account", - "ledgers.accounts.subtitle": "{count} {count, plural, =0 {accounts found} one {acount found} other {accounts found}}", + "ledgers.accounts.subtitle": "{count} {count, plural, =0 {accounts found} one {account found} other {accounts found}}", "ledgers.accounts.title": "Accounts", "ledgers.assets.createButton": "Create your first Asset", "ledgers.assets.emptyResource": "You haven't added any Asset within this Ledger yet.", @@ -199,12 +200,14 @@ "ledgers.products.sheet.title": "New Product", "ledgers.products.subtitle": "Clustering or allocation of customers at different levels.", "ledgers.products.title": "Products", + "ledgers.sheet.tabs.details": "Ledger Details", "ledgers.sheetCreate.description": "Fill in the data of the Ledger you wish to create.", "ledgers.sheetCreate.title": "New Ledger", "ledgers.subtitle": "Visualize and edit the Ledgers of your Organization.", + "ledgers.tab.accounts": "Accounts", "ledgers.tab.assets": "Assets", "ledgers.tab.overview": "Overview", - "ledgers.tab.portfolios-and-accounts": "Portfolios and Accounts", + "ledgers.tab.portfolios": "Portfolios", "ledgers.tab.products": "Products", "ledgers.title": "Ledgers", "ledgers.toast.accountCreated": "{accountName} account successfully created", @@ -249,6 +252,7 @@ "products.delete.description": "You are about to permanently delete this product. This action cannot be undone. Do you wish to continue?", "settings.tab.organizations": "Organizations", "settings.tab.otherSettings": "Other Settings", + "settings.tab.portfolios": "Portfolios", "settings.tab.products": "Products", "settings.tabs.organizations": "Organizations", "settings.tabs.others": "Others Settings", diff --git a/locales/extracted/pt.json b/locales/extracted/pt.json index ff403cbb..07b01f0d 100644 --- a/locales/extracted/pt.json +++ b/locales/extracted/pt.json @@ -168,7 +168,6 @@ "ledgers.portfolio.sheet.edit.description": "Visualize e edite os campos do portfólio.", "ledgers.portfolio.sheet.edit.title": "Editar {portfolioName}", "ledgers.portfolio.sheet.title": "Novo Portfólio", - "ledgers.tab.portfolios-and-accounts": "Portfólios e Contas", "portfolio.create": "Criar primeiro portfólio", "entity.portfolio.description": "Insira o identificador único da entidade associado a este portfólio", "entity.portfolio.entityId": "ID da Entidade", @@ -279,5 +278,10 @@ "ledgers.assets.sheet.tabs.details": "Detalhes do Ativo", "ledgers.portfolio.sheet.tabs.details": "Detalhes do Portfólio", "ledgers.products.sheet.tabs.details": "Detalhes do Produto", - "notFound.backToHome": "Voltar para Home" + "notFound.backToHome": "Voltar para Home", + "ledgers.sheet.tabs.details": "Detalhes do Ledger", + "common.records": "registros", + "ledgers.tab.accounts": "Contas", + "ledgers.tab.portfolios": "Portfólios", + "settings.tab.portfolios": "Portfólios" } diff --git a/package.json b/package.json index 6d4f9cbd..a7f75b4e 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "build": "next build", "start": "next start -p 8081", "lint": "next lint --fix", - "storybook": "storybook dev -p 6006", + "storybook": "storybook dev -p 6007", "build-storybook": "storybook build", "extract:i18n": "tsx ./scripts/i18n-extract.ts", "compile:i18n": "formatjs compile-folder \"./locales/extracted\" --format simple \"./locales/compiled\"", diff --git a/src/app/(auth-routes)/signin/page.tsx b/src/app/(auth-routes)/signin/page.tsx index 9cfbeb34..044c458e 100644 --- a/src/app/(auth-routes)/signin/page.tsx +++ b/src/app/(auth-routes)/signin/page.tsx @@ -16,6 +16,7 @@ import { ArrowRight } from 'lucide-react' import React from 'react' import LoadingScreen from '@/components/loading-screen' import MidazLogo from '@/images/midaz-login-screen.png' +import BackgroundImage from '@/images/login-wallpaper.jpg' import { Tooltip, TooltipContent, @@ -172,7 +173,13 @@ const SignInPage = () => { -
+
+ Login background image
Midaz Logo diff --git a/src/app/(routes)/ledgers/[id]/accounts-and-portfolios/accounts-portfolios-tab-content.tsx b/src/app/(routes)/ledgers/[id]/accounts-and-portfolios/accounts-portfolios-tab-content.tsx deleted file mode 100644 index 8917e8c2..00000000 --- a/src/app/(routes)/ledgers/[id]/accounts-and-portfolios/accounts-portfolios-tab-content.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { AccountsContent } from './accounts-content' -import { PortfoliosContent } from './portfolios-content' - -export const AccountsPortfoliosTabContent = () => { - return ( -
- - -
- ) -} diff --git a/src/app/(routes)/ledgers/[id]/accounts-and-portfolios/portfolios-content.tsx b/src/app/(routes)/ledgers/[id]/accounts-and-portfolios/portfolios-content.tsx deleted file mode 100644 index 96fc9cb3..00000000 --- a/src/app/(routes)/ledgers/[id]/accounts-and-portfolios/portfolios-content.tsx +++ /dev/null @@ -1,334 +0,0 @@ -import { Button } from '@/components/ui/button' -import { MoreVertical, Plus, ChevronDown, ChevronUp } from 'lucide-react' -import { PortfolioSheet } from './portfolios-sheet' -import { useParams } from 'next/navigation' -import { EntityBox } from '@/components/entity-box' -import { useCreateUpdateSheet } from '@/components/sheet/use-create-update-sheet' -import { PortfolioResponseDto } from '@/core/application/dto/portfolios-dto' -import { useDeletePortfolio, useListPortfolios } from '@/client/portfolios' -import { useOrganization } from '@/context/organization-provider/organization-provider-client' -import { useIntl } from 'react-intl' -import { isNil } from 'lodash' -import { - getCoreRowModel, - getFilteredRowModel, - useReactTable -} from '@tanstack/react-table' -import React, { useState } from 'react' -import { useConfirmDialog } from '@/components/confirmation-dialog/use-confirm-dialog' -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger -} from '@/components/ui/dropdown-menu' -import { - Table, - TableBody, - TableCell, - TableContainer, - TableHead, - TableHeader, - TableRow -} from '@/components/ui/table' -import { truncateString } from '@/helpers' -import ConfirmationDialog from '@/components/confirmation-dialog' -import { Skeleton } from '@/components/ui/skeleton' -import { useAllPortfoliosAccounts } from '@/client/accounts' -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger -} from '@/components/ui/tooltip' -import useCustomToast from '@/hooks/use-custom-toast' -import { Arrow } from '@radix-ui/react-tooltip' - -export const PortfoliosContent = () => { - const intl = useIntl() - const { id: ledgerId } = useParams<{ id: string }>() - const { currentOrganization } = useOrganization() - const [columnFilters, setColumnFilters] = React.useState([]) - const [isTableExpanded, setIsTableExpanded] = useState(false) - const { showInfo } = useCustomToast() - - const { - data: portfoliosData, - refetch, - isLoading - } = useAllPortfoliosAccounts({ - organizationId: currentOrganization.id!, - ledgerId: ledgerId - }) - - const { mutate: deletePortfolio, isPending: deletePending } = - useDeletePortfolio({ - organizationId: currentOrganization.id!, - ledgerId, - onSuccess: () => { - handleDialogClose() - refetch() - } - }) - - const { handleDialogOpen, dialogProps, handleDialogClose } = useConfirmDialog( - { - onConfirm: (id: string) => deletePortfolio({ id }) - } - ) - - const { handleCreate, handleEdit, sheetProps } = - useCreateUpdateSheet() - - const table = useReactTable({ - data: portfoliosData?.items!, - columns: [ - { - accessorKey: 'name' - } - ], - getCoreRowModel: getCoreRowModel(), - getFilteredRowModel: getFilteredRowModel(), - onColumnFiltersChange: setColumnFilters, - state: { - columnFilters - } - }) - - if (isLoading) { - return - } - const handleCopyToClipboard = (value: string, message: string) => { - navigator.clipboard.writeText(value) - showInfo(message) - } - - return ( - <> - - - - - - - - - {!isNil(portfoliosData?.items) && - portfoliosData?.items.length > 0 && ( - - )} - - - - {!isNil(portfoliosData?.items) && - portfoliosData?.items.length > 0 && - isTableExpanded && ( - - - - - - {intl.formatMessage({ - id: 'common.id', - defaultMessage: 'ID' - })} - - - {intl.formatMessage({ - id: 'common.name', - defaultMessage: 'Name' - })} - - - {intl.formatMessage({ - id: 'common.metadata', - defaultMessage: 'Metadata' - })} - - - {intl.formatMessage({ - id: 'common.accounts', - defaultMessage: 'Accounts' - })} - - - {intl.formatMessage({ - id: 'common.actions', - defaultMessage: 'Actions' - })} - - - - - {table.getRowModel().rows.map((portfolio) => { - const displayId = - portfolio.original.id && portfolio.original.id.length > 8 - ? `${truncateString(portfolio.original.id, 8)}` - : portfolio.original.id - - return ( - - - - - - handleCopyToClipboard( - portfolio.original.id, - intl.formatMessage({ - id: 'ledgers.toast.copyId', - defaultMessage: - 'The id has been copied to your clipboard.' - }) - ) - } - > -

- {displayId} -

-
- -

- {portfolio.original.id} -

-

- {intl.formatMessage({ - id: 'ledgers.columnsTable.tooltipCopyText', - defaultMessage: 'Click to copy' - })} -

- -
-
-
-
- {portfolio.original.name} - - {intl.formatMessage( - { - id: 'common.table.accounts', - defaultMessage: - '{number, plural, =0 {No accounts} one {# account} other {# accounts}}' - }, - { - number: portfolio.original.accounts?.length || 0 - } - )} - - - {intl.formatMessage( - { - id: 'common.table.metadata', - defaultMessage: - '{number, plural, =0 {-} one {# record} other {# records}}' - }, - { - number: Object.entries( - portfolio.original.metadata || [] - ).length - } - )} - - - - - - - - - - handleEdit({ - ...portfolio.original, - entityId: portfolio.original.id, - status: { - ...portfolio.original.status, - description: - portfolio.original.status.description ?? - '' - } - } as PortfolioResponseDto) - } - > - {intl.formatMessage({ - id: `common.edit`, - defaultMessage: 'Edit' - })} - - - { - handleDialogOpen(portfolio?.original?.id!) - }} - > - {intl.formatMessage({ - id: `common.delete`, - defaultMessage: 'Delete' - })} - - - - -
- ) - })} -
-
-
- )} - - ) -} diff --git a/src/app/(routes)/ledgers/[id]/accounts-and-portfolios/accounts-data-table.tsx b/src/app/(routes)/ledgers/[id]/accounts/accounts-data-table.tsx similarity index 67% rename from src/app/(routes)/ledgers/[id]/accounts-and-portfolios/accounts-data-table.tsx rename to src/app/(routes)/ledgers/[id]/accounts/accounts-data-table.tsx index 6e44835c..65171473 100644 --- a/src/app/(routes)/ledgers/[id]/accounts-and-portfolios/accounts-data-table.tsx +++ b/src/app/(routes)/ledgers/[id]/accounts/accounts-data-table.tsx @@ -41,7 +41,6 @@ type AccountsTableProps = { } onDelete: (id: string, account: AccountType) => void refetch: () => void - isTableExpanded: boolean handleEdit: (account: AccountType) => void } @@ -118,11 +117,13 @@ const AccountRow: React.FC = ({ ) )} - {account.original.portfolioName} + + {account.original.portfolio?.name ?? } + - @@ -155,7 +156,6 @@ export const AccountsDataTable: React.FC = ({ accounts, table, onDelete, - isTableExpanded, handleEdit }) => { const intl = useIntl() @@ -167,66 +167,64 @@ export const AccountsDataTable: React.FC = ({ } return ( - <> - {!isNil(accounts?.items) && - accounts?.items.length > 0 && - isTableExpanded && ( - - - - - - {intl.formatMessage({ - id: 'common.id', - defaultMessage: 'ID' - })} - - - {intl.formatMessage({ - id: 'entity.account.name', - defaultMessage: 'Account Name' - })} - - - {intl.formatMessage({ - id: 'entity.account.currency', - defaultMessage: 'Assets' - })} - - - {intl.formatMessage({ - id: 'common.metadata', - defaultMessage: 'Metadata' - })} - - - {intl.formatMessage({ - id: 'common.portfolio', - defaultMessage: 'Portfolio' - })} - - - {intl.formatMessage({ - id: 'common.actions', - defaultMessage: 'Actions' - })} - - - - - {table.getRowModel().rows.map((account) => ( - - ))} - -
-
- )} - + + {!isNil(accounts?.items) && accounts?.items.length > 0 && ( + + + + + + {intl.formatMessage({ + id: 'common.id', + defaultMessage: 'ID' + })} + + + {intl.formatMessage({ + id: 'entity.account.name', + defaultMessage: 'Account Name' + })} + + + {intl.formatMessage({ + id: 'entity.account.currency', + defaultMessage: 'Assets' + })} + + + {intl.formatMessage({ + id: 'common.metadata', + defaultMessage: 'Metadata' + })} + + + {intl.formatMessage({ + id: 'common.portfolio', + defaultMessage: 'Portfolio' + })} + + + {intl.formatMessage({ + id: 'common.actions', + defaultMessage: 'Actions' + })} + + + + + {table.getRowModel().rows.map((account) => ( + + ))} + +
+
+ )} +
) } diff --git a/src/app/(routes)/ledgers/[id]/accounts-and-portfolios/accounts-sheet.tsx b/src/app/(routes)/ledgers/[id]/accounts/accounts-sheet.tsx similarity index 77% rename from src/app/(routes)/ledgers/[id]/accounts-and-portfolios/accounts-sheet.tsx rename to src/app/(routes)/ledgers/[id]/accounts/accounts-sheet.tsx index 9e7160c1..8cfa1cb7 100644 --- a/src/app/(routes)/ledgers/[id]/accounts-and-portfolios/accounts-sheet.tsx +++ b/src/app/(routes)/ledgers/[id]/accounts/accounts-sheet.tsx @@ -20,7 +20,7 @@ import { MetadataField } from '@/components/form/metadata-field' import { useListProducts } from '@/client/products' import { useCreateAccount, useUpdateAccount } from '@/client/accounts' import { useListPortfolios } from '@/client/portfolios' -import { isNil } from 'lodash' +import { isNil, omitBy } from 'lodash' import { useListAssets } from '@/client/assets' import useCustomToast from '@/hooks/use-custom-toast' import { accountSchema } from '@/schema/account' @@ -34,12 +34,13 @@ export type AccountSheetProps = DialogProps & { ledgerId: string mode: 'create' | 'edit' data?: AccountType | null - onSucess?: () => void + onSuccess?: () => void } -const defaultValues = { +const initialValues = { name: '', entityId: '', + portfolioId: '', productId: '', assetCode: '', alias: '', @@ -52,7 +53,7 @@ type FormData = z.infer export const AccountSheet = ({ mode, data, - onSucess, + onSuccess, onOpenChange, ...others }: AccountSheetProps) => { @@ -104,21 +105,14 @@ export const AccountSheet = ({ const form = useForm>({ resolver: zodResolver(accountSchema), - defaultValues: Object.assign( - {}, - defaultValues, - mode === 'create' ? { entityId: '' } : {} - ) + defaultValues: initialValues }) - const selectedPortfolioId = form.watch('portfolioId') - const { mutate: createAccount, isPending: createPending } = useCreateAccount({ organizationId: currentOrganization.id!, ledgerId, - portfolioId: selectedPortfolioId, onSuccess: (data) => { - onSucess?.() + onSuccess?.() onOpenChange?.(false) showSuccess( intl.formatMessage( @@ -144,9 +138,8 @@ export const AccountSheet = ({ organizationId: currentOrganization.id!, ledgerId, accountId: data?.id!, - portfolioId: data?.portfolioId!, onSuccess: (data) => { - onSucess?.() + onSuccess?.() onOpenChange?.(false) showSuccess( intl.formatMessage( @@ -171,19 +164,21 @@ export const AccountSheet = ({ const { showSuccess, showError } = useCustomToast() const handleSubmit = (data: FormData) => { + const cleanedData = omitBy(data, (value) => value === '' || isNil(value)) + if (mode === 'create') { createAccount({ - ...data, - portfolioId: data.portfolioId + ...cleanedData, + portfolioId: cleanedData.portfolioId }) } else if (mode === 'edit') { - const { portfolioId, assetCode, ...updateData } = data + const { type, portfolioId, assetCode, ...updateData } = cleanedData updateAccount({ ...updateData }) } - form.reset(defaultValues) + form.reset(initialValues) } React.useEffect(() => { @@ -198,7 +193,7 @@ export const AccountSheet = ({ }, [data]) return ( - <> + e.preventDefault()}> {mode === 'create' && ( @@ -290,78 +285,76 @@ export const AccountSheet = ({ })} /> - - - {intl.formatMessage({ - id: 'account.sheet.type.deposit', - defaultMessage: 'Deposit' - })} - - - - {intl.formatMessage({ - id: 'account.sheet.type.savings', - defaultMessage: 'Savings' - })} - - - - {intl.formatMessage({ - id: 'account.sheet.type.loans', - defaultMessage: 'Loans' - })} - - - - {intl.formatMessage({ - id: 'account.sheet.type.marketplace', - defaultMessage: 'Marketplace' - })} - - - - {intl.formatMessage({ - id: 'account.sheet.type.creditCard', - defaultMessage: 'CreditCard' - })} - - - - {intl.formatMessage({ - id: 'account.sheet.type.external', - defaultMessage: 'External' - })} - - - {mode === 'create' && ( - - )} - {mode === 'create' && ( - <> + + + + {intl.formatMessage({ + id: 'account.sheet.type.deposit', + defaultMessage: 'Deposit' + })} + + + + {intl.formatMessage({ + id: 'account.sheet.type.savings', + defaultMessage: 'Savings' + })} + + + + {intl.formatMessage({ + id: 'account.sheet.type.loans', + defaultMessage: 'Loans' + })} + + + + {intl.formatMessage({ + id: 'account.sheet.type.marketplace', + defaultMessage: 'Marketplace' + })} + + + + {intl.formatMessage({ + id: 'account.sheet.type.creditCard', + defaultMessage: 'CreditCard' + })} + + + + {intl.formatMessage({ + id: 'account.sheet.type.external', + defaultMessage: 'External' + })} + + + + ))} - + )} @@ -458,6 +450,6 @@ export const AccountSheet = ({ - + ) } diff --git a/src/app/(routes)/ledgers/[id]/accounts-and-portfolios/accounts-content.tsx b/src/app/(routes)/ledgers/[id]/accounts/accounts-tab-content.tsx similarity index 64% rename from src/app/(routes)/ledgers/[id]/accounts-and-portfolios/accounts-content.tsx rename to src/app/(routes)/ledgers/[id]/accounts/accounts-tab-content.tsx index f7cc2008..e74e63ca 100644 --- a/src/app/(routes)/ledgers/[id]/accounts-and-portfolios/accounts-content.tsx +++ b/src/app/(routes)/ledgers/[id]/accounts/accounts-tab-content.tsx @@ -1,13 +1,11 @@ -import React, { useState, useMemo, useCallback } from 'react' +import React, { useMemo } from 'react' import { Button } from '@/components/ui/button' -import { ChevronDown, ChevronUp, Plus } from 'lucide-react' +import { Plus } from 'lucide-react' import { useParams } from 'next/navigation' import { EntityBox } from '@/components/entity-box' import { useCreateUpdateSheet } from '@/components/sheet/use-create-update-sheet' -import { useListPortfolios } from '@/client/portfolios' import { useOrganization } from '@/context/organization-provider/organization-provider-client' import { useIntl } from 'react-intl' -import { isNil } from 'lodash' import { getCoreRowModel, getFilteredRowModel, @@ -15,47 +13,41 @@ import { } from '@tanstack/react-table' import { useConfirmDialog } from '@/components/confirmation-dialog/use-confirm-dialog' import ConfirmationDialog from '@/components/confirmation-dialog' -import { useAllPortfoliosAccounts, useDeleteAccount } from '@/client/accounts' +import { useAccountsWithPortfolios, useDeleteAccount } from '@/client/accounts' import { Skeleton } from '@/components/ui/skeleton' import useCustomToast from '@/hooks/use-custom-toast' import { AccountType } from '@/types/accounts-type' import { AccountSheet } from './accounts-sheet' import { AccountsDataTable } from './accounts-data-table' -export const AccountsContent = () => { +export const AccountsTabContent = () => { const intl = useIntl() const { id: ledgerId } = useParams<{ id: string }>() const { currentOrganization } = useOrganization() const [columnFilters, setColumnFilters] = React.useState([]) - const [isTableExpanded, setIsTableExpanded] = useState(false) - - const { data, refetch, isLoading } = useListPortfolios({ - organizationId: currentOrganization.id!, - ledgerId: ledgerId - }) const { data: accountsData, refetch: refetchAccounts, isLoading: isAccountsLoading - } = useAllPortfoliosAccounts({ + } = useAccountsWithPortfolios({ organizationId: currentOrganization.id!, ledgerId: ledgerId }) - const accountsList = useMemo( - () => ({ - items: - accountsData?.items.flatMap((portfolio) => - portfolio.accounts.map((account) => ({ - ...account, - portfolioName: portfolio.name, - portfolioId: portfolio.id - })) - ) || [] - }), - [accountsData] - ) + const accountsList: AccountType[] = useMemo(() => { + return ( + accountsData?.items.map((account: any) => ({ + ...account, + assetCode: account.assetCode, + parentAccountId: account.parentAccountId, + productId: account.productId, + metadata: account.metadata, + portfolioId: account.portfolio?.id, + portfolioName: account.portfolio?.name + })) || [] + ) + }, [accountsData]) const { showSuccess, showError } = useCustomToast() @@ -73,7 +65,6 @@ export const AccountsContent = () => { organizationId: currentOrganization.id!, ledgerId, accountId: selectedAccount?.id || '', - portfolioId: selectedAccount?.portfolioId || '', onSuccess: () => { handleDialogClose() refetchAccounts() @@ -83,7 +74,7 @@ export const AccountsContent = () => { id: 'ledgers.toast.accountDeleted', defaultMessage: '{accountName} account successfully deleted' }, - { accountName: (selectedAccount as AccountType)?.name! } + { accountName: selectedAccount?.name! } ) ) }, @@ -104,18 +95,58 @@ export const AccountsContent = () => { } = useCreateUpdateSheet() const handleEdit = (account: AccountType) => { - handleEditOriginal(account as unknown as AccountType) + handleEditOriginal(account) } const table = useReactTable({ - data: accountsList?.items!, + data: accountsList, columns: [ - { accessorKey: 'id' }, - { accessorKey: 'name' }, - { accessorKey: 'assets' }, - { accessorKey: 'metadata' }, - { accessorKey: 'portfolio' }, - { accessorKey: 'actions' } + { accessorKey: 'id', header: 'ID' }, + { + accessorKey: 'name', + header: intl.formatMessage({ + id: 'entity.account.name', + defaultMessage: 'Account Name' + }) + }, + { + accessorKey: 'assetCode', + header: intl.formatMessage({ + id: 'entity.account.currency', + defaultMessage: 'Assets' + }), + cell: (info) => info.getValue() || '-' + }, + { + accessorKey: 'metadata', + header: intl.formatMessage({ + id: 'common.metadata', + defaultMessage: 'Metadata' + }), + cell: (info) => { + const metadata = info.getValue() || {} + const count = Object.keys(metadata).length + return count > 0 + ? `${count} ${intl.formatMessage({ id: 'common.records', defaultMessage: 'records' })}` + : '-' + } + }, + { + accessorKey: 'portfolio.name', + header: intl.formatMessage({ + id: 'common.portfolio', + defaultMessage: 'Portfolio' + }), + cell: (info) => info.getValue() || '-' + }, + { + accessorKey: 'actions', + header: intl.formatMessage({ + id: 'common.actions', + defaultMessage: 'Actions' + }), + cell: () => null + } ], getCoreRowModel: getCoreRowModel(), getFilteredRowModel: getFilteredRowModel(), @@ -125,12 +156,12 @@ export const AccountsContent = () => { } }) - if (isLoading) { + if (isAccountsLoading) { return } return ( - <> + { @@ -157,15 +188,15 @@ export const AccountsContent = () => { defaultMessage: 'Accounts' })} subtitle={ - accountsList?.items?.length !== undefined + accountsList.length !== undefined ? intl.formatMessage( { id: 'ledgers.accounts.subtitle', defaultMessage: - '{count} {count, plural, =0 {accounts found} one {acount found} other {accounts found}}' + '{count} {count, plural, =0 {accounts found} one {account found} other {accounts found}}' }, { - count: accountsList.items.length + count: accountsList.length } ) : undefined @@ -175,9 +206,9 @@ export const AccountsContent = () => { - {!isNil(accountsList?.items) && accountsList?.items?.length > 0 && ( - - )} {accountsList && ( )} - + ) } diff --git a/src/app/(routes)/ledgers/[id]/ledger-details-view.tsx b/src/app/(routes)/ledgers/[id]/ledger-details-view.tsx index 07a0b486..222cdfc1 100644 --- a/src/app/(routes)/ledgers/[id]/ledger-details-view.tsx +++ b/src/app/(routes)/ledgers/[id]/ledger-details-view.tsx @@ -9,7 +9,6 @@ import { cn } from '@/lib/utils' import { useFormState } from '@/context/form-details-context' import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs' import { useIntl } from 'react-intl' -import { AccountsPortfoliosTabContent } from './accounts-and-portfolios/accounts-portfolios-tab-content' import { ProductsTabContent } from './products/products-tab-content' import { useTabs } from '@/hooks/use-tabs' import { getBreadcrumbPaths } from '@/components/breadcrumb/get-breadcrumb-paths' @@ -19,12 +18,15 @@ import { useUpdateLedger } from '@/client/ledgers' import { LedgerDetailsSkeleton } from './ledger-details-skeleton' import { OverviewTabContent } from './overview/overview-tab-content' import { AssetsTabContent } from './assets/assets-tab-content' +import { PortfoliosTabContent } from './portfolios/portfolios-tab-content' +import { AccountsTabContent } from './accounts/accounts-tab-content' const TAB_VALUES = { OVERVIEW: 'overview', ASSETS: 'assets', - PORTFOLIOS_AND_ACCOUNTS: 'portfolios-and-accounts', - PRODUCTS: 'products' + PRODUCTS: 'products', + PORTFOLIOS: 'portfolios', + ACCOUNTS: 'accounts' } const DEFAULT_TAB_VALUE = TAB_VALUES.OVERVIEW @@ -94,17 +96,24 @@ const LedgerDetailsView = ({ data }: LedgerDetailsViewProps) => { }, { name: intl.formatMessage({ - id: `ledgers.tab.portfolios-and-accounts`, - defaultMessage: 'Portfolios and Accounts' + id: `settings.tab.products`, + defaultMessage: 'Products' }), - active: () => activeTab === TAB_VALUES.PORTFOLIOS_AND_ACCOUNTS + active: () => activeTab === TAB_VALUES.PRODUCTS }, { name: intl.formatMessage({ - id: `settings.tab.products`, - defaultMessage: 'Products' + id: `settings.tab.portfolios`, + defaultMessage: 'Portfolios' }), - active: () => activeTab === TAB_VALUES.PRODUCTS + active: () => activeTab === TAB_VALUES.PORTFOLIOS + }, + { + name: intl.formatMessage({ + id: `ledgers.tab.accounts`, + defaultMessage: 'Accounts' + }), + active: () => activeTab === TAB_VALUES.ACCOUNTS } ]) @@ -160,17 +169,24 @@ const LedgerDetailsView = ({ data }: LedgerDetailsViewProps) => { })} - + + {intl.formatMessage({ + id: 'ledgers.tab.products', + defaultMessage: 'Products' + })} + + + {intl.formatMessage({ - id: 'ledgers.tab.portfolios-and-accounts', - defaultMessage: 'Portfolios and Accounts' + id: 'ledgers.tab.portfolios', + defaultMessage: 'Portfolios' })} - + {intl.formatMessage({ - id: 'ledgers.tab.products', - defaultMessage: 'Products' + id: 'ledgers.tab.accounts', + defaultMessage: 'Accounts' })} @@ -183,13 +199,17 @@ const LedgerDetailsView = ({ data }: LedgerDetailsViewProps) => { - - - - + + + + + + + + { + const intl = useIntl() + const { id: ledgerId } = useParams<{ id: string }>() + const { currentOrganization } = useOrganization() + const [columnFilters, setColumnFilters] = React.useState([]) + const { showInfo } = useCustomToast() + + const { + data: portfoliosData, + refetch, + isLoading + } = usePortfoliosWithAccounts({ + organizationId: currentOrganization.id!, + ledgerId: ledgerId + }) + + const { mutate: deletePortfolio, isPending: deletePending } = + useDeletePortfolio({ + organizationId: currentOrganization.id!, + ledgerId, + onSuccess: () => { + handleDialogClose() + refetch() + } + }) + + const { handleDialogOpen, dialogProps, handleDialogClose } = useConfirmDialog( + { + onConfirm: (id: string) => deletePortfolio({ id }) + } + ) + + const { handleCreate, handleEdit, sheetProps } = + useCreateUpdateSheet() + + const table = useReactTable({ + data: portfoliosData?.items!, + columns: [ + { + accessorKey: 'name' + } + ], + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnFiltersChange: setColumnFilters, + state: { + columnFilters + } + }) + + if (isLoading) { + return + } + const handleCopyToClipboard = (value: string, message: string) => { + navigator.clipboard.writeText(value) + showInfo(message) + } + + return ( + <> + + + + + + + + + + + + {!isNil(portfoliosData?.items) && portfoliosData?.items.length > 0 && ( + + + + + + {intl.formatMessage({ + id: 'common.id', + defaultMessage: 'ID' + })} + + + {intl.formatMessage({ + id: 'common.name', + defaultMessage: 'Name' + })} + + + {intl.formatMessage({ + id: 'common.metadata', + defaultMessage: 'Metadata' + })} + + + {intl.formatMessage({ + id: 'common.accounts', + defaultMessage: 'Accounts' + })} + + + {intl.formatMessage({ + id: 'common.actions', + defaultMessage: 'Actions' + })} + + + + + {table.getRowModel().rows.map((portfolio) => { + const metadataCount = Object.entries( + portfolio.original.metadata || [] + ).length + const displayId = + portfolio.original.id && portfolio.original.id.length > 8 + ? `${truncateString(portfolio.original.id, 8)}` + : portfolio.original.id + + return ( + + + + + + handleCopyToClipboard( + portfolio.original.id, + intl.formatMessage({ + id: 'ledgers.toast.copyId', + defaultMessage: + 'The id has been copied to your clipboard.' + }) + ) + } + > +

+ {displayId} +

+
+ +

+ {portfolio.original.id} +

+

+ {intl.formatMessage({ + id: 'ledgers.columnsTable.tooltipCopyText', + defaultMessage: 'Click to copy' + })} +

+ +
+
+
+
+ {portfolio.original.name} + + {metadataCount === 0 ? ( + + ) : ( + intl.formatMessage( + { + id: 'common.table.metadata', + defaultMessage: + '{number, plural, =0 {-} one {# record} other {# records}}' + }, + { + number: metadataCount + } + ) + )} + + + {intl.formatMessage( + { + id: 'common.table.accounts', + defaultMessage: + '{number, plural, =0 {No accounts} one {# account} other {# accounts}}' + }, + { + number: portfolio.original.accounts?.length || 0 + } + )} + + + + + + + + + + handleEdit({ + ...portfolio.original, + entityId: portfolio.original.id, + status: { + ...portfolio.original.status, + description: + portfolio.original.status.description ?? '' + } + } as PortfolioResponseDto) + } + > + {intl.formatMessage({ + id: `common.edit`, + defaultMessage: 'Edit' + })} + + + { + handleDialogOpen(portfolio?.original?.id!) + }} + > + {intl.formatMessage({ + id: `common.delete`, + defaultMessage: 'Delete' + })} + + + + +
+ ) + })} +
+
+
+ )} + + ) +} diff --git a/src/app/(routes)/ledgers/ledgers-sheet.tsx b/src/app/(routes)/ledgers/ledgers-sheet.tsx index fea264fc..d463595e 100644 --- a/src/app/(routes)/ledgers/ledgers-sheet.tsx +++ b/src/app/(routes)/ledgers/ledgers-sheet.tsx @@ -1,7 +1,6 @@ import { InputField } from '@/components/form' import { MetadataField } from '@/components/form/metadata-field' import { Form } from '@/components/ui/form' -import { Label } from '@/components/ui/label' import { Sheet, SheetContent, @@ -10,7 +9,6 @@ import { SheetHeader, SheetTitle } from '@/components/ui/sheet' -import { Switch } from '@/components/ui/switch' import { ledger } from '@/schema/ledger' import { zodResolver } from '@hookform/resolvers/zod' import { DialogProps } from '@radix-ui/react-dialog' @@ -25,6 +23,7 @@ import { LedgerResponseDto } from '@/core/application/dto/ledger-response-dto' import { useOrganization } from '@/context/organization-provider/organization-provider-client' import useCustomToast from '@/hooks/use-custom-toast' import { ILedgerType } from '@/types/ledgers-type' +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' export type LedgersSheetProps = DialogProps & { mode: 'create' | 'edit' @@ -87,10 +86,6 @@ export const LedgersSheet = ({ defaultValues: Object.assign({}, defaultValues, ledger) }) - const [metadataEnabled, setMetadataEnabled] = React.useState( - Object.entries(ledger?.metadata || {}).length > 0 - ) - const handleSubmit = (data: FormData) => { if (mode === 'create') { createLedger(data) @@ -107,10 +102,7 @@ export const LedgersSheet = ({ React.useEffect(() => { if (!isNil(data)) { - setMetadataEnabled(Object.entries(data.metadata).length > 0) form.reset(data, { keepDefaultValues: true }) - } else { - setMetadataEnabled(false) } }, [data]) @@ -137,47 +129,48 @@ export const LedgersSheet = ({
- - -
-
- - setMetadataEnabled(!metadataEnabled)} - /> -
-
- - {metadataEnabled && ( -
+ + + +
+ + +

+ {intl.formatMessage({ + id: 'common.requiredFields', + defaultMessage: '(*) required fields.' + })} +

+
+
+ -
- )} - -

- {intl.formatMessage({ - id: 'common.requiredFields', - defaultMessage: '(*) required fields.' - })} -

+ + (FetchAccountsWithPortfoliosUseCase) + +export async function GET( + request: Request, + { params }: { params: { id: string; ledgerId: string; portfolioId: string } } +) { + try { + const { searchParams } = new URL(request.url) + const limit = Number(searchParams.get('limit')) || 100 + const page = Number(searchParams.get('page')) || 1 + const organizationId = params.id + + const accountsWithPortfolios = + await fetchAccountsWithPortfoliosUseCase.execute( + organizationId, + params.ledgerId, + limit, + page + ) + + return NextResponse.json(accountsWithPortfolios) + } catch (error: any) { + console.error('Error fetching accounts with portfolios', error) + + const { message, status } = await apiErrorHandler(error) + + return NextResponse.json({ message }, { status }) + } +} diff --git a/src/app/api/organizations/[id]/ledgers/[ledgerId]/portfolios/[portfolioId]/accounts/[accountId]/route.ts b/src/app/api/organizations/[id]/ledgers/[ledgerId]/accounts/[accountId]/route.ts similarity index 85% rename from src/app/api/organizations/[id]/ledgers/[ledgerId]/portfolios/[portfolioId]/accounts/[accountId]/route.ts rename to src/app/api/organizations/[id]/ledgers/[ledgerId]/accounts/[accountId]/route.ts index 78d179c6..2ebfc20c 100644 --- a/src/app/api/organizations/[id]/ledgers/[ledgerId]/portfolios/[portfolioId]/accounts/[accountId]/route.ts +++ b/src/app/api/organizations/[id]/ledgers/[ledgerId]/accounts/[accountId]/route.ts @@ -32,18 +32,16 @@ export async function GET( params: { id: string ledgerId: string - portfolioId: string accountId: string } } ) { try { - const { id: organizationId, ledgerId, portfolioId, accountId } = params + const { id: organizationId, ledgerId, accountId } = params const account = await getAccountByIdUseCase.execute( organizationId, ledgerId, - portfolioId, accountId ) @@ -64,19 +62,17 @@ export async function PATCH( params: { id: string ledgerId: string - portfolioId: string accountId: string } } ) { try { const body = await request.json() - const { id: organizationId, ledgerId, portfolioId, accountId } = params + const { id: organizationId, ledgerId, accountId } = params const accountUpdated = await updateAccountUseCase.execute( organizationId, ledgerId, - portfolioId, accountId, body ) @@ -98,7 +94,6 @@ export async function DELETE( params: { id: string ledgerId: string - portfolioId: string accountId: string } } @@ -106,15 +101,9 @@ export async function DELETE( try { const organizationId = params.id! const ledgerId = params.ledgerId - const portfolioId = params.portfolioId const accountId = params.accountId - await deleteAccountUseCase.execute( - organizationId, - ledgerId, - portfolioId, - accountId - ) + await deleteAccountUseCase.execute(organizationId, ledgerId, accountId) return NextResponse.json({}, { status: 200 }) } catch (error: any) { diff --git a/src/app/api/organizations/[id]/ledgers/[ledgerId]/portfolios/[portfolioId]/accounts/route.ts b/src/app/api/organizations/[id]/ledgers/[ledgerId]/accounts/route.ts similarity index 83% rename from src/app/api/organizations/[id]/ledgers/[ledgerId]/portfolios/[portfolioId]/accounts/route.ts rename to src/app/api/organizations/[id]/ledgers/[ledgerId]/accounts/route.ts index e9f38cfc..d040fdf0 100644 --- a/src/app/api/organizations/[id]/ledgers/[ledgerId]/portfolios/[portfolioId]/accounts/route.ts +++ b/src/app/api/organizations/[id]/ledgers/[ledgerId]/accounts/route.ts @@ -11,7 +11,6 @@ import { import { NextResponse } from 'next/server' -// Update use case references const createAccountUseCase: CreateAccount = container.get(CreateAccountUseCase) @@ -26,23 +25,25 @@ export async function GET( params: { id: string ledgerId: string - organizationId: string - portfolioId: string } } ) { try { const { searchParams } = new URL(request.url) + const { id: organizationId, ledgerId } = params const limit = Number(searchParams.get('limit')) || 10 const page = Number(searchParams.get('page')) || 1 - const organizationId = params.id - const ledgerId = params.ledgerId - const portfolioId = params.portfolioId + + if (!organizationId || !ledgerId) { + return NextResponse.json( + { message: 'Missing required parameters' }, + { status: 400 } + ) + } const accounts = await fetchAllAccountsUseCase.execute( organizationId, ledgerId, - portfolioId, page, limit ) @@ -59,18 +60,17 @@ export async function GET( export async function POST( request: Request, - { params }: { params: { id: string; ledgerId: string; portfolioId: string } } + { params }: { params: { id: string; ledgerId: string } } ) { try { const body = await request.json() + console.log(body) const organizationId = params.id const ledgerId = params.ledgerId - const portfolioId = params.portfolioId const account = await createAccountUseCase.execute( organizationId, ledgerId, - portfolioId, body ) diff --git a/src/app/api/organizations/[id]/ledgers/[ledgerId]/portfolios-accounts/route.ts b/src/app/api/organizations/[id]/ledgers/[ledgerId]/portfolios-accounts/route.ts new file mode 100644 index 00000000..693e957e --- /dev/null +++ b/src/app/api/organizations/[id]/ledgers/[ledgerId]/portfolios-accounts/route.ts @@ -0,0 +1,39 @@ +import { container } from '@/core/infrastructure/container-registry/container-registry' +import { apiErrorHandler } from '@/app/api/utils/api-error-handler' +import { NextResponse } from 'next/server' +import { + FetchPortfoliosWithAccounts, + FetchPortfoliosWithAccountsUseCase +} from '@/core/application/use-cases/portfolios-with-accounts/fetch-portfolios-with-account-use-case' + +const fetchPortfoliosWithAccountsUseCase = + container.get(FetchPortfoliosWithAccountsUseCase) + +export async function GET( + request: Request, + { params }: { params: { id: string; ledgerId: string } } +) { + try { + const { searchParams } = new URL(request.url) + const limit = Number(searchParams.get('limit')) || 100 + const page = Number(searchParams.get('page')) || 1 + const organizationId = params.id + const ledgerId = params.ledgerId + + const portfoliosWithAccounts = + await fetchPortfoliosWithAccountsUseCase.execute( + organizationId, + ledgerId, + limit, + page + ) + + return NextResponse.json(portfoliosWithAccounts) + } catch (error: any) { + console.error('Error fetching portfolios with accounts', error) + + const { message, status } = await apiErrorHandler(error) + + return NextResponse.json({ message }, { status }) + } +} diff --git a/src/app/api/organizations/[id]/ledgers/[ledgerId]/portfolios/portfolios-accounts/route.ts b/src/app/api/organizations/[id]/ledgers/[ledgerId]/portfolios/portfolios-accounts/route.ts deleted file mode 100644 index ed751f27..00000000 --- a/src/app/api/organizations/[id]/ledgers/[ledgerId]/portfolios/portfolios-accounts/route.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { container } from '@/core/infrastructure/container-registry/container-registry' -import { apiErrorHandler } from '@/app/api/utils/api-error-handler' -import { - FetchAllPortfoliosAccounts, - FetchAllPortfoliosAccountsUseCase -} from '@/core/application/use-cases/portfolios-accounts/fetch-portfolios-accounts-use-case' -import { NextResponse } from 'next/server' - -const fetchAllPortfoliosAccountsUseCases = - container.get(FetchAllPortfoliosAccountsUseCase) - -export async function GET( - request: Request, - { params }: { params: { id: string; ledgerId: string; portfolioId: string } } -) { - try { - const { searchParams } = new URL(request.url) - const limit = Number(searchParams.get('limit')) || 10 - const page = Number(searchParams.get('page')) || 1 - const organizationId = params.id - - const ledgers = await fetchAllPortfoliosAccountsUseCases.execute( - organizationId, - params.ledgerId, - limit, - page - ) - - return NextResponse.json(ledgers) - } catch (error: any) { - console.error('Error fetching all ledgers', error) - - const { message, status } = await apiErrorHandler(error) - - return NextResponse.json({ message }, { status }) - } -} diff --git a/src/client/accounts.ts b/src/client/accounts.ts index 627d80c4..c46abee4 100644 --- a/src/client/accounts.ts +++ b/src/client/accounts.ts @@ -1,5 +1,5 @@ +import { AccountResponseDto } from '@/core/application/dto/account-dto' import { PaginationDto } from '@/core/application/dto/pagination-dto' -import { PortfolioViewResponseDTO } from '@/core/application/dto/portfolio-view-dto' import { AccountEntity } from '@/core/domain/entities/account-entity' import { deleteFetcher, @@ -10,45 +10,42 @@ import { import { useMutation, UseMutationOptions, - useQuery, - UseQueryOptions + useQuery } from '@tanstack/react-query' type UseListAccountsProps = { organizationId: string ledgerId: string - portfolioId: string } export const useListAccounts = ({ organizationId, ledgerId, - portfolioId, ...options }: UseListAccountsProps) => { return useQuery>({ - queryKey: [organizationId, ledgerId, portfolioId, 'accounts'], + queryKey: [organizationId, ledgerId, 'accounts'], queryFn: getFetcher( - `/api/organizations/${organizationId}/ledgers/${ledgerId}/portfolios/${portfolioId}/accounts` + `/api/organizations/${organizationId}/ledgers/${ledgerId}/accounts` ), ...options }) } -type UseAllPortfoliosAccountsProps = { +type UseAccountsWithPortfoliosProps = { organizationId: string ledgerId: string } -export const useAllPortfoliosAccounts = ({ +export const useAccountsWithPortfolios = ({ organizationId, ledgerId, ...options -}: UseAllPortfoliosAccountsProps) => { - return useQuery>({ - queryKey: [organizationId, ledgerId, 'portfolios-accounts'], +}: UseAccountsWithPortfoliosProps) => { + return useQuery>({ + queryKey: [organizationId, ledgerId, 'accounts-with-portfolios'], queryFn: getFetcher( - `/api/organizations/${organizationId}/ledgers/${ledgerId}/portfolios/portfolios-accounts` + `/api/organizations/${organizationId}/ledgers/${ledgerId}/accounts-portfolios` ), ...options }) @@ -57,21 +54,19 @@ export const useAllPortfoliosAccounts = ({ type UseDeleteAccountProps = UseMutationOptions & { organizationId: string ledgerId: string - portfolioId: string accountId: string } export const useDeleteAccount = ({ organizationId, ledgerId, - portfolioId, accountId, ...options }: UseDeleteAccountProps) => { return useMutation({ - mutationKey: [organizationId, ledgerId, portfolioId, accountId], + mutationKey: [organizationId, ledgerId, accountId], mutationFn: deleteFetcher( - `/api/organizations/${organizationId}/ledgers/${ledgerId}/portfolios/${portfolioId}/accounts` + `/api/organizations/${organizationId}/ledgers/${ledgerId}/accounts` ), ...options }) @@ -80,19 +75,17 @@ export const useDeleteAccount = ({ type UseCreateAccountProps = UseMutationOptions & { organizationId: string ledgerId: string - portfolioId: string } export const useCreateAccount = ({ organizationId, ledgerId, - portfolioId, ...options }: UseCreateAccountProps) => { return useMutation({ - mutationKey: [organizationId, ledgerId, portfolioId], + mutationKey: [organizationId, ledgerId], mutationFn: postFetcher( - `/api/organizations/${organizationId}/ledgers/${ledgerId}/portfolios/${portfolioId}/accounts` + `/api/organizations/${organizationId}/ledgers/${ledgerId}/accounts` ), ...options }) @@ -101,21 +94,19 @@ export const useCreateAccount = ({ type UseUpdateAccountProps = UseMutationOptions & { organizationId: string ledgerId: string - portfolioId: string accountId: string } export const useUpdateAccount = ({ organizationId, ledgerId, - portfolioId, accountId, ...options }: UseUpdateAccountProps) => { return useMutation({ - mutationKey: [organizationId, ledgerId, portfolioId, accountId], + mutationKey: [organizationId, ledgerId, accountId], mutationFn: patchFetcher( - `/api/organizations/${organizationId}/ledgers/${ledgerId}/portfolios/${portfolioId}/accounts/${accountId}` + `/api/organizations/${organizationId}/ledgers/${ledgerId}/accounts/${accountId}` ), ...options }) diff --git a/src/client/portfolios.ts b/src/client/portfolios.ts index 17dd047e..ad3457de 100644 --- a/src/client/portfolios.ts +++ b/src/client/portfolios.ts @@ -1,8 +1,6 @@ import { PaginationDto } from '@/core/application/dto/pagination-dto' -import { - PortfolioResponseDto, - UpdatePortfolioDto -} from '@/core/application/dto/portfolios-dto' +import { PortfolioViewResponseDTO } from '@/core/application/dto/portfolio-view-dto' +import { PortfolioResponseDto } from '@/core/application/dto/portfolios-dto' import { PortfolioEntity } from '@/core/domain/entities/portfolios-entity' import { getFetcher, @@ -21,6 +19,25 @@ type UseCreatePortfolioProps = UseMutationOptions & { ledgerId: string } +type UsePortfoliosWithAccountsProps = { + organizationId: string + ledgerId: string +} + +export const usePortfoliosWithAccounts = ({ + organizationId, + ledgerId, + ...options +}: UsePortfoliosWithAccountsProps) => { + return useQuery>({ + queryKey: [organizationId, ledgerId, 'portfolios-with-accounts'], + queryFn: getFetcher( + `/api/organizations/${organizationId}/ledgers/${ledgerId}/portfolios-accounts` + ), + ...options + }) +} + export const useCreatePortfolio = ({ organizationId, ledgerId, diff --git a/src/components/form/input-field/input-field.mdx b/src/components/form/input-field/input-field.mdx new file mode 100644 index 00000000..15d8a2e9 --- /dev/null +++ b/src/components/form/input-field/input-field.mdx @@ -0,0 +1,35 @@ +import { Meta, Controls, Primary, Canvas, Stories } from '@storybook/blocks' +import * as Story from './input-field.stories' + + + +# InputField + +An implementation for easy use of Input component. + +### Basic + + + +### Required + + + +### With Tooltip + + + +### With Label Extra + + + +### Read Only + + + +### Disabled + + + + + diff --git a/src/components/form/input-field/input-field.stories.tsx b/src/components/form/input-field/input-field.stories.tsx new file mode 100644 index 00000000..bfb7158d --- /dev/null +++ b/src/components/form/input-field/input-field.stories.tsx @@ -0,0 +1,69 @@ +import { Meta, StoryObj } from '@storybook/react' +import { InputField, InputFieldProps } from '.' +import { useForm } from 'react-hook-form' +import { Form } from '@/components/ui/form' + +const meta: Meta = { + title: 'Components/Form/InputField', + component: InputField, + argTypes: {} +} + +export default meta + +function BaseComponent(args: Omit) { + const form = useForm() + + return ( +
+ + + +
+ ) +} + +export const Primary: StoryObj = { + render: (args) => BaseComponent(args) +} + +export const Required: StoryObj = { + args: { + required: true + }, + render: (args) => BaseComponent(args) +} + +export const WithTooltip: StoryObj = { + args: { + tooltip: 'This is a Tooltip!' + }, + render: (args) => BaseComponent(args) +} + +export const WithExtraLabel: StoryObj = { + args: { + labelExtra: Extra Label + }, + render: (args) => BaseComponent(args) +} + +export const ReadOnly: StoryObj = { + args: { + readOnly: true + }, + render: (args) => BaseComponent(args) +} + +export const Disabled: StoryObj = { + args: { + disabled: true + }, + render: (args) => BaseComponent(args) +} diff --git a/src/components/form/select-field/select-field.mdx b/src/components/form/select-field/select-field.mdx new file mode 100644 index 00000000..5bb5eb58 --- /dev/null +++ b/src/components/form/select-field/select-field.mdx @@ -0,0 +1,31 @@ +import { Meta, Controls, Primary, Canvas, Stories } from '@storybook/blocks' +import * as Story from './select-field.stories' + + + +# SelectField + +An implementation for easy use of Select component. + +### Basic + + + +### Required + + + +### With Tooltip + + + +### With Label Extra + + + +### Disabled + + + + + diff --git a/src/components/form/select-field/select-field.stories.tsx b/src/components/form/select-field/select-field.stories.tsx new file mode 100644 index 00000000..8ac87bb2 --- /dev/null +++ b/src/components/form/select-field/select-field.stories.tsx @@ -0,0 +1,67 @@ +import { Meta, StoryObj } from '@storybook/react' +import { SelectField, SelectFieldProps } from '.' +import { useForm } from 'react-hook-form' +import { Form } from '@/components/ui/form' +import { SelectItem } from '@/components/ui/select' + +const meta: Meta = { + title: 'Components/Form/SelectField', + component: SelectField, + argTypes: {} +} + +export default meta + +function BaseComponent(args: Omit) { + const form = useForm() + + return ( +
+
+ + Apple + Banana + Orange + +
+
+ ) +} + +export const Primary: StoryObj = { + render: (args) => BaseComponent(args) +} + +export const Required: StoryObj = { + args: { + required: true + }, + render: (args) => BaseComponent(args) +} + +export const WithTooltip: StoryObj = { + args: { + tooltip: 'This is a Tooltip!' + }, + render: (args) => BaseComponent(args) +} + +export const WithExtraLabel: StoryObj = { + args: { + labelExtra: Extra Label + }, + render: (args) => BaseComponent(args) +} + +export const Disabled: StoryObj = { + args: { + disabled: true + }, + render: (args) => BaseComponent(args) +} diff --git a/src/components/loading-screen.tsx b/src/components/loading-screen.tsx index 650f19b0..35dbc4cb 100644 --- a/src/components/loading-screen.tsx +++ b/src/components/loading-screen.tsx @@ -1,6 +1,8 @@ 'use client' import React from 'react' +import Image from 'next/image' +import LoadingImage from '@/images/loading-wallpaper.jpg' import midazLoading from '@/animations/midaz-loading.json' import { Lottie } from '@/lib/lottie' @@ -10,7 +12,8 @@ type LoadingScreenProps = { const LoadingScreen = ({ onComplete }: LoadingScreenProps) => { return ( -
+
+ Loading image
+ +# Alert + +Displays a callout for user attention. + +### Primary + + + +### Destructive + + + + + diff --git a/src/components/ui/alert/alert.stories.tsx b/src/components/ui/alert/alert.stories.tsx new file mode 100644 index 00000000..6506a80a --- /dev/null +++ b/src/components/ui/alert/alert.stories.tsx @@ -0,0 +1,36 @@ +import { Meta, StoryObj } from '@storybook/react' +import { Alert, AlertDescription, AlertProps, AlertTitle } from '.' +import { Terminal } from 'lucide-react' + +const meta: Meta = { + title: 'Primitives/Alert', + component: Alert, + argTypes: {} +} + +export default meta + +export const Primary: StoryObj = { + render: (args) => ( + + + Heads up! + This is an alert component + + ) +} + +export const Destructive: StoryObj = { + args: { + variant: 'destructive' + }, + render: (args) => ( + + + Error + + Your session has expired. Please log in again. + + + ) +} diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert/index.tsx similarity index 80% rename from src/components/ui/alert.tsx rename to src/components/ui/alert/index.tsx index 65019c6d..6fd7a005 100644 --- a/src/components/ui/alert.tsx +++ b/src/components/ui/alert/index.tsx @@ -19,17 +19,19 @@ const alertVariants = cva( } ) -const Alert = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes & VariantProps ->(({ className, variant, ...props }, ref) => ( -
-)) +export type AlertProps = React.HTMLAttributes & + VariantProps + +const Alert = React.forwardRef( + ({ className, variant, ...props }, ref) => ( +
+ ) +) Alert.displayName = 'Alert' const AlertTitle = React.forwardRef< diff --git a/src/components/ui/button/button.mdx b/src/components/ui/button/button.mdx index 17704d0e..9707f609 100644 --- a/src/components/ui/button/button.mdx +++ b/src/components/ui/button/button.mdx @@ -27,9 +27,17 @@ This is a button component. +### Full Width + + + ### With leading icon +### With leading icon at end + + + diff --git a/src/components/ui/button/button.stories.tsx b/src/components/ui/button/button.stories.tsx index 35f8a6cd..02744bb0 100644 --- a/src/components/ui/button/button.stories.tsx +++ b/src/components/ui/button/button.stories.tsx @@ -58,9 +58,26 @@ export const Outline: StoryObj = { } } +export const FullWidth: StoryObj = { + args: { + fullWidth: true, + children: 'Button' + }, + render: (args) => + + + + Edit profile + + Make changes to your profile here. Click save when you are done. + + + + + + + + ) +} diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog/index.tsx similarity index 100% rename from src/components/ui/dialog.tsx rename to src/components/ui/dialog/index.tsx diff --git a/src/components/ui/popover.tsx b/src/components/ui/popover/index.tsx similarity index 100% rename from src/components/ui/popover.tsx rename to src/components/ui/popover/index.tsx diff --git a/src/components/ui/popover/popover.mdx b/src/components/ui/popover/popover.mdx new file mode 100644 index 00000000..cf9574be --- /dev/null +++ b/src/components/ui/popover/popover.mdx @@ -0,0 +1,12 @@ +import { Meta, Controls, Primary, Canvas } from '@storybook/blocks' +import * as Story from './popover.stories' + + + +# Popover + +Displays rich content in a portal, triggered by a button. + +### Primary + + diff --git a/src/components/ui/popover/popover.stories.tsx b/src/components/ui/popover/popover.stories.tsx new file mode 100644 index 00000000..3c556fd9 --- /dev/null +++ b/src/components/ui/popover/popover.stories.tsx @@ -0,0 +1,25 @@ +import { Popover, PopoverContent, PopoverTrigger } from '.' +import { Meta, StoryObj } from '@storybook/react' +import { Button } from '../button' +import { PopoverProps } from '@radix-ui/react-popover' + +const meta: Meta = { + title: 'Primitives/Popover', + component: Popover, + argTypes: {} +} + +export default meta + +export const Primary: StoryObj = { + render: (args) => ( + + + + + +

This is a Popover!

+
+
+ ) +} diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet/index.tsx similarity index 100% rename from src/components/ui/sheet.tsx rename to src/components/ui/sheet/index.tsx diff --git a/src/components/ui/sheet/sheet.mdx b/src/components/ui/sheet/sheet.mdx new file mode 100644 index 00000000..0af9243b --- /dev/null +++ b/src/components/ui/sheet/sheet.mdx @@ -0,0 +1,20 @@ +import { Meta, Controls, Primary, Canvas } from '@storybook/blocks' +import * as Story from './sheet.stories' + + + +# Dialog + +Extends the Dialog component to display content that complements the main content of the screen. \ +[Docs](https://www.radix-ui.com/primitives/docs/components/dialog) + + + +### Sides + +Use the side property to SheetContent to indicate the edge of the screen where the component will appear. The values can be top, right, bottom or left. + + + + + diff --git a/src/components/ui/sheet/sheet.stories.tsx b/src/components/ui/sheet/sheet.stories.tsx new file mode 100644 index 00000000..694c8dd9 --- /dev/null +++ b/src/components/ui/sheet/sheet.stories.tsx @@ -0,0 +1,77 @@ +import { + Sheet, + SheetClose, + SheetContent, + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, + SheetTrigger +} from '.' +import { Meta, StoryObj } from '@storybook/react' +import { Button } from '../button' + +const meta: Meta = { + title: 'Primitives/Sheet', + component: Sheet, + argTypes: {} +} + +export default meta + +export const Primary: StoryObj = { + render: (args) => ( + + Open + + + Are you absolutely sure? + + This action cannot be undone. This will permanently delete your + account and remove your data from our servers. + + + + + ) +} + +const SHEET_SIDES = ['top', 'right', 'bottom', 'left'] as const + +type SheetSide = (typeof SHEET_SIDES)[number] + +export const SheetSide: StoryObj = { + render: (args) => { + return ( +
+ {SHEET_SIDES.map((side) => ( + + + + + + + Edit profile + + Make changes to your profile here. Click save when you are + done. + + +
+

Content

+

Content

+

Content

+

Content

+
+ + + + + +
+
+ ))} +
+ ) + } +} diff --git a/src/components/ui/switch.tsx b/src/components/ui/switch/index.tsx similarity index 100% rename from src/components/ui/switch.tsx rename to src/components/ui/switch/index.tsx diff --git a/src/components/ui/switch/switch.mdx b/src/components/ui/switch/switch.mdx new file mode 100644 index 00000000..d9abc06c --- /dev/null +++ b/src/components/ui/switch/switch.mdx @@ -0,0 +1,20 @@ +import { Meta, Controls, Primary, Canvas } from '@storybook/blocks' +import * as Story from './switch.stories' + + + +# Switch + +A control that allows the user to toggle between checked and not checked. \ +[Docs](https://www.radix-ui.com/primitives/docs/components/switch) + +### Primary + + + +### With Text + + + + + diff --git a/src/components/ui/switch/switch.stories.tsx b/src/components/ui/switch/switch.stories.tsx new file mode 100644 index 00000000..86f5942b --- /dev/null +++ b/src/components/ui/switch/switch.stories.tsx @@ -0,0 +1,25 @@ +import { Meta, StoryObj } from '@storybook/react' +import { Switch } from '.' +import { SwitchProps } from '@radix-ui/react-switch' +import { Label } from '../label' + +const meta: Meta = { + title: 'Primitives/Switch', + component: Switch, + argTypes: {} +} + +export default meta + +export const Primary: StoryObj = { + render: (args) => +} + +export const WithText: StoryObj = { + render: (args) => ( +
+ + +
+ ) +} diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip/index.tsx similarity index 100% rename from src/components/ui/tooltip.tsx rename to src/components/ui/tooltip/index.tsx diff --git a/src/components/ui/tooltip/tooltip.mdx b/src/components/ui/tooltip/tooltip.mdx new file mode 100644 index 00000000..6dad9494 --- /dev/null +++ b/src/components/ui/tooltip/tooltip.mdx @@ -0,0 +1,29 @@ +import { Meta, Controls, Primary, Canvas } from '@storybook/blocks' +import * as Story from './tooltip.stories' + + + +# Tooltip + +A window overlaid on either the primary window or another dialog window, +rendering the content underneath inert. \ +[Docs](https://www.radix-ui.com/primitives/docs/components/tooltip) + +### Primary + + + +### Delay + +The delay can be changed to zero (instantly) too. + + + +### Position + +Tooltip can be displayed in all sides of the trigger component. + + + + + diff --git a/src/components/ui/tooltip/tooltip.stories.tsx b/src/components/ui/tooltip/tooltip.stories.tsx new file mode 100644 index 00000000..be57da36 --- /dev/null +++ b/src/components/ui/tooltip/tooltip.stories.tsx @@ -0,0 +1,92 @@ +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '.' +import { Meta, StoryObj } from '@storybook/react' +import { Button } from '../button' +import { TooltipProps } from '@radix-ui/react-tooltip' + +const meta: Meta = { + title: 'Primitives/Tooltip', + component: Tooltip, + argTypes: {} +} + +export default meta + +export const Primary: StoryObj = { + render: (args) => ( + + + + + + +

Add to library

+
+
+
+ ) +} + +export const WithoutDelay: StoryObj = { + args: { + delayDuration: 0 + }, + render: (args) => ( + + + + + + +

Add to library

+
+
+
+ ) +} + +export const WithSides: StoryObj = { + render: (args) => ( +
+ + + + + + +

This is a Tooltip!

+
+
+
+ + + + + + +

This is a Tooltip!

+
+
+
+ + + + + + +

This is a Tooltip!

+
+
+
+ + + + + + +

This is a Tooltip!

+
+
+
+
+ ) +} diff --git a/src/core/application/dto/account-dto.ts b/src/core/application/dto/account-dto.ts index 4c91f3c2..6f5667e1 100644 --- a/src/core/application/dto/account-dto.ts +++ b/src/core/application/dto/account-dto.ts @@ -7,6 +7,7 @@ export interface CreateAccountDto { type: string entityId?: string | null parentAccountId?: string | null + portfolioId?: string | null productId?: string status: { code: string @@ -25,7 +26,7 @@ export interface AccountResponseDto { type: string entityId: string parentAccountId: string - portfolioId: string + portfolioId?: string | null productId: string status: StatusDto metadata: Record @@ -42,6 +43,7 @@ export interface UpdateAccountDto { entityId?: string parentAccountId?: string productId?: string + portfolioId?: string status?: StatusDto metadata?: Record } diff --git a/src/core/application/dto/portfolios-dto.ts b/src/core/application/dto/portfolios-dto.ts index 5b3c4f9c..e333250a 100644 --- a/src/core/application/dto/portfolios-dto.ts +++ b/src/core/application/dto/portfolios-dto.ts @@ -1,3 +1,4 @@ +import { AccountResponseDto } from './account-dto' import { StatusDto } from './status.dto' export interface CreatePortfolioDto { @@ -20,6 +21,7 @@ export interface PortfolioResponseDto { createdAt: Date updatedAt: Date deletedAt: Date | null + accounts?: AccountResponseDto[] } export interface UpdatePortfolioDto { diff --git a/src/core/application/mappers/account-mapper.ts b/src/core/application/mappers/account-mapper.ts index 3465d2c2..10cc51ba 100644 --- a/src/core/application/mappers/account-mapper.ts +++ b/src/core/application/mappers/account-mapper.ts @@ -28,7 +28,7 @@ export class AccountMapper { assetCode: account.assetCode, parentAccountId: account.parentAccountId!, productId: account.productId!, - portfolioId: account.portfolioId! + portfolioId: account.portfolioId } } @@ -44,6 +44,7 @@ export class AccountMapper { status: dto.status!, parentAccountId: dto.parentAccountId, productId: dto.productId, + portfolioId: dto.portfolioId, metadata: dto.metadata ?? {} } } diff --git a/src/core/application/mappers/portfolio-mapper.ts b/src/core/application/mappers/portfolio-mapper.ts index 64b16c1d..3d0b6d8c 100644 --- a/src/core/application/mappers/portfolio-mapper.ts +++ b/src/core/application/mappers/portfolio-mapper.ts @@ -4,6 +4,8 @@ import { RequestContextManager } from '@/lib/logger/request-context' import { PaginationEntity } from '@/core/domain/entities/pagination-entity' import { PaginationMapper } from './pagination-mapper' +import { AccountMapper } from './account-mapper' +import { AccountResponseDto } from '../dto/account-dto' export class PortfolioMapper { public static toDomain(dto: CreatePortfolioDto): PortfolioEntity { @@ -49,4 +51,14 @@ export class PortfolioMapper { ): PaginationEntity { return PaginationMapper.toResponseDto(result, PortfolioMapper.toResponseDto) } + + public static toDtoWithAccounts( + portfolio: PortfolioEntity, + accounts: AccountResponseDto[] + ): PortfolioResponseDto { + return { + ...PortfolioMapper.toResponseDto(portfolio), + accounts: accounts.map(AccountMapper.toDto) + } + } } diff --git a/src/core/application/use-cases/portfolios-accounts/fetch-portfolios-accounts-use-case.ts b/src/core/application/use-cases/accounts-with-portfolios/fetch-accounts-with-portfolios-use-case.ts similarity index 50% rename from src/core/application/use-cases/portfolios-accounts/fetch-portfolios-accounts-use-case.ts rename to src/core/application/use-cases/accounts-with-portfolios/fetch-accounts-with-portfolios-use-case.ts index b15e7d87..92ba4bd2 100644 --- a/src/core/application/use-cases/portfolios-accounts/fetch-portfolios-accounts-use-case.ts +++ b/src/core/application/use-cases/accounts-with-portfolios/fetch-accounts-with-portfolios-use-case.ts @@ -1,14 +1,14 @@ import { FetchAllAccountsRepository } from '@/core/domain/repositories/accounts/fetch-all-accounts-repository' +import { FetchAllPortfoliosRepository } from '@/core/domain/repositories/portfolios/fetch-all-portfolio-repository' import { PaginationDto } from '../../dto/pagination-dto' import { PaginationEntity } from '@/core/domain/entities/pagination-entity' -import { FetchAllPortfoliosRepository } from '@/core/domain/repositories/portfolios/fetch-all-portfolio-repository' import { PortfolioEntity } from '@/core/domain/entities/portfolios-entity' import { AccountEntity } from '@/core/domain/entities/account-entity' import { PortfolioViewResponseDTO } from '../../dto/portfolio-view-dto' import { AccountMapper } from '../../mappers/account-mapper' import { inject, injectable } from 'inversify' -export interface FetchAllPortfoliosAccounts { +export interface FetchAccountsWithPortfolios { execute: ( organizationId: string, ledgerId: string, @@ -18,8 +18,8 @@ export interface FetchAllPortfoliosAccounts { } @injectable() -export class FetchAllPortfoliosAccountsUseCase - implements FetchAllPortfoliosAccounts +export class FetchAccountsWithPortfoliosUseCase + implements FetchAccountsWithPortfolios { constructor( @inject(FetchAllPortfoliosRepository) @@ -34,6 +34,14 @@ export class FetchAllPortfoliosAccountsUseCase limit: number, page: number ): Promise> { + const accountsResult: PaginationEntity = + await this.fetchAllAccountsRepository.fetchAll( + organizationId, + ledgerId, + limit, + page + ) + const portfoliosResult: PaginationEntity = await this.fetchAllPortfoliosRepository.fetchAll( organizationId, @@ -42,48 +50,35 @@ export class FetchAllPortfoliosAccountsUseCase page ) - let portfoliosAccountResponseDTO: PaginationDto = - { - items: [], - limit: portfoliosResult.limit, - page: portfoliosResult.page + const portfolioMap = new Map() + portfoliosResult.items.forEach((portfolio) => { + if (portfolio.id) { + portfolioMap.set(portfolio.id, portfolio) } + }) - const portfolioItems = portfoliosResult.items || [] + const accountsWithPortfolio: any[] = accountsResult.items.map((account) => { + const portfolio = account.portfolioId + ? portfolioMap.get(account.portfolioId) + : null - portfoliosAccountResponseDTO.items = await Promise.all( - portfolioItems.map(async (portfolio) => { - const accountsResult: PaginationEntity = - await this.fetchAllAccountsRepository.fetchAll( - organizationId, - ledgerId, - portfolio.id!, - limit, - page - ) - - const portfolioAccounts: PortfolioViewResponseDTO = { - id: portfolio.id!, - ledgerId: portfolio.ledgerId!, - organizationId: portfolio.organizationId!, - name: portfolio.name!, - status: { - code: portfolio.status!.code!, - description: portfolio.status!.description! - }, - metadata: portfolio.metadata!, - createdAt: portfolio.createdAt!, - updatedAt: portfolio.updatedAt!, - deletedAt: portfolio.deletedAt!, - accounts: accountsResult.items - ? accountsResult.items.map(AccountMapper.toDto) - : [] - } + return { + ...AccountMapper.toDto(account), + portfolio: portfolio + ? { + id: portfolio.id!, + name: portfolio.name! + } + : null + } + }) - return portfolioAccounts - }) - ) + const responseDTO: PaginationDto = { + items: accountsWithPortfolio, + limit: accountsResult.limit, + page: accountsResult.page + } - return portfoliosAccountResponseDTO + return responseDTO } } diff --git a/src/core/application/use-cases/accounts/create-account-use-case.ts b/src/core/application/use-cases/accounts/create-account-use-case.ts index c1578f00..a6d14ea0 100644 --- a/src/core/application/use-cases/accounts/create-account-use-case.ts +++ b/src/core/application/use-cases/accounts/create-account-use-case.ts @@ -8,7 +8,6 @@ export interface CreateAccount { execute: ( organizationId: string, ledgerId: string, - portfolioId: string, account: CreateAccountDto ) => Promise } @@ -23,7 +22,6 @@ export class CreateAccountUseCase implements CreateAccount { async execute( organizationId: string, ledgerId: string, - portfolioId: string, account: CreateAccountDto ): Promise { account.status = { @@ -34,7 +32,6 @@ export class CreateAccountUseCase implements CreateAccount { const accountCreated = await this.createAccountRepository.create( organizationId, ledgerId, - portfolioId, accountEntity ) diff --git a/src/core/application/use-cases/accounts/delete-account-use-case.ts b/src/core/application/use-cases/accounts/delete-account-use-case.ts index a5bbd7a4..145517a0 100644 --- a/src/core/application/use-cases/accounts/delete-account-use-case.ts +++ b/src/core/application/use-cases/accounts/delete-account-use-case.ts @@ -5,7 +5,6 @@ export interface DeleteAccount { execute: ( organizationId: string, ledgerId: string, - portfolioId: string, accountId: string ) => Promise } @@ -20,13 +19,11 @@ export class DeleteAccountUseCase implements DeleteAccount { async execute( organizationId: string, ledgerId: string, - portfolioId: string, accountId: string ): Promise { await this.deleteAccountRepository.delete( organizationId, ledgerId, - portfolioId, accountId ) } diff --git a/src/core/application/use-cases/accounts/fetch-account-by-id-use-case.ts b/src/core/application/use-cases/accounts/fetch-account-by-id-use-case.ts index 73f14ea1..d9266ac6 100644 --- a/src/core/application/use-cases/accounts/fetch-account-by-id-use-case.ts +++ b/src/core/application/use-cases/accounts/fetch-account-by-id-use-case.ts @@ -7,7 +7,6 @@ export interface FetchAccountById { execute: ( organizationId: string, ledgerId: string, - portfolioId: string, accountId: string ) => Promise } @@ -22,13 +21,11 @@ export class FetchAccountByIdUseCase implements FetchAccountById { async execute( organizationId: string, ledgerId: string, - portfolioId: string, accountId: string ): Promise { const account = await this.fetchAccountByIdRepository.fetchById( organizationId, ledgerId, - portfolioId, accountId ) diff --git a/src/core/application/use-cases/accounts/fetch-all-account-use-case.ts b/src/core/application/use-cases/accounts/fetch-all-account-use-case.ts index 6c9dce29..185cc9b9 100644 --- a/src/core/application/use-cases/accounts/fetch-all-account-use-case.ts +++ b/src/core/application/use-cases/accounts/fetch-all-account-use-case.ts @@ -10,7 +10,6 @@ export interface FetchAllAccounts { execute: ( organizationId: string, ledgerId: string, - portfolioId: string, limit: number, page: number ) => Promise> @@ -26,7 +25,6 @@ export class FetchAllAccountsUseCase implements FetchAllAccounts { async execute( organizationId: string, ledgerId: string, - portfolioId: string, limit: number, page: number ): Promise> { @@ -34,7 +32,6 @@ export class FetchAllAccountsUseCase implements FetchAllAccounts { await this.fetchAllAccountsRepository.fetchAll( organizationId, ledgerId, - portfolioId, page, limit ) diff --git a/src/core/application/use-cases/accounts/update-account-use-case.ts b/src/core/application/use-cases/accounts/update-account-use-case.ts index fb7163de..b0ce6ea2 100644 --- a/src/core/application/use-cases/accounts/update-account-use-case.ts +++ b/src/core/application/use-cases/accounts/update-account-use-case.ts @@ -8,7 +8,6 @@ export interface UpdateAccount { execute: ( organizationId: string, ledgerId: string, - portfolioId: string, accountId: string, account: Partial ) => Promise @@ -24,7 +23,6 @@ export class UpdateAccountUseCase implements UpdateAccount { async execute( organizationId: string, ledgerId: string, - portfolioId: string, accountId: string, account: Partial ): Promise { @@ -40,7 +38,6 @@ export class UpdateAccountUseCase implements UpdateAccount { await this.updateAccountRepository.update( organizationId, ledgerId, - portfolioId, accountId, accountEntity ) diff --git a/src/core/application/use-cases/portfolios-with-accounts/fetch-portfolios-with-account-use-case.tsx b/src/core/application/use-cases/portfolios-with-accounts/fetch-portfolios-with-account-use-case.tsx new file mode 100644 index 00000000..90ded390 --- /dev/null +++ b/src/core/application/use-cases/portfolios-with-accounts/fetch-portfolios-with-account-use-case.tsx @@ -0,0 +1,69 @@ +import { FetchAllAccountsRepository } from '@/core/domain/repositories/accounts/fetch-all-accounts-repository' +import { FetchAllPortfoliosRepository } from '@/core/domain/repositories/portfolios/fetch-all-portfolio-repository' +import { PaginationDto } from '../../dto/pagination-dto' +import { AccountMapper } from '../../mappers/account-mapper' +import { inject, injectable } from 'inversify' +import { groupBy } from 'lodash' +import { PortfolioMapper } from '../../mappers/portfolio-mapper' + +export interface FetchPortfoliosWithAccounts { + execute: ( + organizationId: string, + ledgerId: string, + limit: number, + page: number + ) => Promise> +} + +@injectable() +export class FetchPortfoliosWithAccountsUseCase + implements FetchPortfoliosWithAccounts +{ + constructor( + @inject(FetchAllPortfoliosRepository) + private readonly fetchAllPortfoliosRepository: FetchAllPortfoliosRepository, + @inject(FetchAllAccountsRepository) + private readonly fetchAllAccountsRepository: FetchAllAccountsRepository + ) {} + + async execute( + organizationId: string, + ledgerId: string, + limit: number, + page: number + ): Promise> { + const portfoliosResult = await this.fetchAllPortfoliosRepository.fetchAll( + organizationId, + ledgerId, + limit, + page + ) + + const allAccountsResult = await this.fetchAllAccountsRepository.fetchAll( + organizationId, + ledgerId, + limit, + page + ) + + const accountsGrouped = groupBy( + allAccountsResult.items, + (account) => account.portfolioId || 'no_portfolio' + ) + + const portfoliosWithAccounts = portfoliosResult.items.map((portfolio) => + PortfolioMapper.toDtoWithAccounts( + portfolio, + (accountsGrouped[portfolio.id!] || []).map(AccountMapper.toDto) + ) + ) + + const responseDTO: PaginationDto = { + items: portfoliosWithAccounts, + limit: portfoliosResult.limit, + page: portfoliosResult.page + } + + return responseDTO + } +} diff --git a/src/core/domain/repositories/accounts/create-accounts-repository.ts b/src/core/domain/repositories/accounts/create-accounts-repository.ts index 10be10cb..090fa776 100644 --- a/src/core/domain/repositories/accounts/create-accounts-repository.ts +++ b/src/core/domain/repositories/accounts/create-accounts-repository.ts @@ -4,7 +4,6 @@ export abstract class CreateAccountsRepository { abstract create: ( organizationId: string, ledgerId: string, - portfolioId: string, account: AccountEntity ) => Promise } diff --git a/src/core/domain/repositories/accounts/delete-accounts-repository.ts b/src/core/domain/repositories/accounts/delete-accounts-repository.ts index d9cd82af..18dd6233 100644 --- a/src/core/domain/repositories/accounts/delete-accounts-repository.ts +++ b/src/core/domain/repositories/accounts/delete-accounts-repository.ts @@ -2,7 +2,6 @@ export abstract class DeleteAccountsRepository { abstract delete: ( organizationId: string, ledgerId: string, - portfolioId: string, accountId: string ) => Promise } diff --git a/src/core/domain/repositories/accounts/fetch-account-by-id-repository.ts b/src/core/domain/repositories/accounts/fetch-account-by-id-repository.ts index 0faf0a7a..0db734b9 100644 --- a/src/core/domain/repositories/accounts/fetch-account-by-id-repository.ts +++ b/src/core/domain/repositories/accounts/fetch-account-by-id-repository.ts @@ -4,7 +4,6 @@ export abstract class FetchAccountByIdRepository { abstract fetchById: ( organizationId: string, ledgerId: string, - portfolioId: string, accountId: string ) => Promise } diff --git a/src/core/domain/repositories/accounts/fetch-all-accounts-repository.ts b/src/core/domain/repositories/accounts/fetch-all-accounts-repository.ts index d2d35b41..1fa71934 100644 --- a/src/core/domain/repositories/accounts/fetch-all-accounts-repository.ts +++ b/src/core/domain/repositories/accounts/fetch-all-accounts-repository.ts @@ -5,7 +5,6 @@ export abstract class FetchAllAccountsRepository { abstract fetchAll: ( organizationId: string, ledgerId: string, - portfolioId: string, limit: number, page: number ) => Promise> diff --git a/src/core/domain/repositories/accounts/update-accounts-repository.ts b/src/core/domain/repositories/accounts/update-accounts-repository.ts index 1dd03d56..c185bb8f 100644 --- a/src/core/domain/repositories/accounts/update-accounts-repository.ts +++ b/src/core/domain/repositories/accounts/update-accounts-repository.ts @@ -4,7 +4,6 @@ export abstract class UpdateAccountsRepository { abstract update: ( organizationId: string, ledgerId: string, - portfolioId: string, accountId: string, account: Partial ) => Promise diff --git a/src/core/infrastructure/container-registry/use-cases/account-module.ts b/src/core/infrastructure/container-registry/use-cases/account-module.ts index c1591ff7..40334e59 100644 --- a/src/core/infrastructure/container-registry/use-cases/account-module.ts +++ b/src/core/infrastructure/container-registry/use-cases/account-module.ts @@ -20,6 +20,10 @@ import { DeleteAccount, DeleteAccountUseCase } from '@/core/application/use-cases/accounts/delete-account-use-case' +import { + FetchAccountsWithPortfolios, + FetchAccountsWithPortfoliosUseCase +} from '@/core/application/use-cases/accounts-with-portfolios/fetch-accounts-with-portfolios-use-case' export const AccountUseCaseModule = new ContainerModule( (container: Container) => { @@ -28,5 +32,8 @@ export const AccountUseCaseModule = new ContainerModule( container.bind(FetchAccountByIdUseCase).toSelf() container.bind(UpdateAccountUseCase).toSelf() container.bind(DeleteAccountUseCase).toSelf() + container + .bind(FetchAccountsWithPortfoliosUseCase) + .toSelf() } ) diff --git a/src/core/infrastructure/container-registry/use-cases/portfolios-module.ts b/src/core/infrastructure/container-registry/use-cases/portfolios-module.ts index 709ef095..2c64a1bb 100644 --- a/src/core/infrastructure/container-registry/use-cases/portfolios-module.ts +++ b/src/core/infrastructure/container-registry/use-cases/portfolios-module.ts @@ -21,9 +21,9 @@ import { FetchPortfolioByIdUseCase } from '@/core/application/use-cases/portfolios/fetch-portfolio-by-id-use-case' import { - FetchAllPortfoliosAccounts, - FetchAllPortfoliosAccountsUseCase -} from '@/core/application/use-cases/portfolios-accounts/fetch-portfolios-accounts-use-case' + FetchPortfoliosWithAccounts, + FetchPortfoliosWithAccountsUseCase +} from '@/core/application/use-cases/portfolios-with-accounts/fetch-portfolios-with-account-use-case' export const PortfolioUseCaseModule = new ContainerModule( (container: Container) => { @@ -32,9 +32,8 @@ export const PortfolioUseCaseModule = new ContainerModule( container.bind(UpdatePortfolioUseCase).toSelf() container.bind(DeletePortfolioUseCase).toSelf() container.bind(FetchPortfolioByIdUseCase).toSelf() - container - .bind(FetchAllPortfoliosAccountsUseCase) + .bind(FetchPortfoliosWithAccountsUseCase) .toSelf() } ) diff --git a/src/core/infrastructure/midaz/accounts/midaz-create-accounts-repository.ts b/src/core/infrastructure/midaz/accounts/midaz-create-accounts-repository.ts index fd6f1f6a..ea68575e 100644 --- a/src/core/infrastructure/midaz/accounts/midaz-create-accounts-repository.ts +++ b/src/core/infrastructure/midaz/accounts/midaz-create-accounts-repository.ts @@ -9,10 +9,9 @@ export class MidazCreateAccountRepository implements CreateAccountsRepository { async create( organizationId: string, ledgerId: string, - portfolioId: string, account: AccountEntity ): Promise { - const url = `${this.baseUrl}/organizations/${organizationId}/ledgers/${ledgerId}/portfolios/${portfolioId}/accounts` + const url = `${this.baseUrl}/organizations/${organizationId}/ledgers/${ledgerId}/accounts` const response = await httpMidazAuthFetch({ url, diff --git a/src/core/infrastructure/midaz/accounts/midaz-delete-accounts-repository.ts b/src/core/infrastructure/midaz/accounts/midaz-delete-accounts-repository.ts index 20cc5026..698da6c6 100644 --- a/src/core/infrastructure/midaz/accounts/midaz-delete-accounts-repository.ts +++ b/src/core/infrastructure/midaz/accounts/midaz-delete-accounts-repository.ts @@ -9,10 +9,9 @@ export class MidazDeleteAccountsRepository implements DeleteAccountsRepository { async delete( organizationId: string, ledgerId: string, - portfolioId: string, accountId: string ): Promise { - const url = `${this.baseUrl}/organizations/${organizationId}/ledgers/${ledgerId}/portfolios/${portfolioId}/accounts/${accountId}` + const url = `${this.baseUrl}/organizations/${organizationId}/ledgers/${ledgerId}/accounts/${accountId}` await httpMidazAuthFetch({ url, method: HTTP_METHODS.DELETE diff --git a/src/core/infrastructure/midaz/accounts/midaz-fetch-account-by-id-repository.ts b/src/core/infrastructure/midaz/accounts/midaz-fetch-account-by-id-repository.ts index 45a991c5..141fd7d0 100644 --- a/src/core/infrastructure/midaz/accounts/midaz-fetch-account-by-id-repository.ts +++ b/src/core/infrastructure/midaz/accounts/midaz-fetch-account-by-id-repository.ts @@ -12,10 +12,9 @@ export class MidazFetchAccountByIdRepository async fetchById( organizationId: string, ledgerId: string, - portfolioId: string, accountId: string ): Promise { - const url = `${this.baseUrl}/organizations/${organizationId}/ledgers/${ledgerId}/portfolios/${portfolioId}/accounts/${accountId}` + const url = `${this.baseUrl}/organizations/${organizationId}/ledgers/${ledgerId}/accounts/${accountId}` const response = await httpMidazAuthFetch({ url, diff --git a/src/core/infrastructure/midaz/accounts/midaz-fetch-all-accounts-repository.ts b/src/core/infrastructure/midaz/accounts/midaz-fetch-all-accounts-repository.ts index 18c44c39..0b47207f 100644 --- a/src/core/infrastructure/midaz/accounts/midaz-fetch-all-accounts-repository.ts +++ b/src/core/infrastructure/midaz/accounts/midaz-fetch-all-accounts-repository.ts @@ -13,11 +13,10 @@ export class MidazFetchAllAccountsRepository async fetchAll( organizationId: string, ledgerId: string, - portfolioId: string, limit: number, page: number ): Promise> { - const url = `${this.baseUrl}/organizations/${organizationId}/ledgers/${ledgerId}/portfolios/${portfolioId}/accounts?limit=${limit}&page=${page}` + const url = `${this.baseUrl}/organizations/${organizationId}/ledgers/${ledgerId}/accounts?limit=${limit}&page=${page}` const response = await httpMidazAuthFetch>({ url, diff --git a/src/core/infrastructure/midaz/accounts/midaz-update-accounts-repository.ts b/src/core/infrastructure/midaz/accounts/midaz-update-accounts-repository.ts index df049e22..1e4412df 100644 --- a/src/core/infrastructure/midaz/accounts/midaz-update-accounts-repository.ts +++ b/src/core/infrastructure/midaz/accounts/midaz-update-accounts-repository.ts @@ -10,11 +10,10 @@ export class MidazUpdateAccountsRepository implements UpdateAccountsRepository { async update( organizationId: string, ledgerId: string, - portfolioId: string, accountId: string, account: Partial ): Promise { - const url = `${this.baseUrl}/organizations/${organizationId}/ledgers/${ledgerId}/portfolios/${portfolioId}/accounts/${accountId}` + const url = `${this.baseUrl}/organizations/${organizationId}/ledgers/${ledgerId}/accounts/${accountId}` const response = await httpMidazAuthFetch({ url, diff --git a/src/schema/account.ts b/src/schema/account.ts index 6e5548f7..a2414745 100644 --- a/src/schema/account.ts +++ b/src/schema/account.ts @@ -3,15 +3,15 @@ import { metadata } from './metadata' const name = z.string().min(3).max(255) -const alias = z.string().min(1).max(255).optional() +const alias = z.string().min(1).max(255) -const entityId = z.string().min(1).max(255).optional() +const entityId = z.string().nullable().optional() const assetCode = z.string() -const portfolioId = z.string() +const portfolioId = z.string().nullable().optional() -const productId = z.string() +const productId = z.string().nullable().optional() const type = z.string() diff --git a/src/types/accounts-type.ts b/src/types/accounts-type.ts index 61062e65..3dd05966 100644 --- a/src/types/accounts-type.ts +++ b/src/types/accounts-type.ts @@ -1,21 +1,24 @@ +import { PortfolioType } from './portfolio-type' + export type AccountType = { id: string ledgerId: string assetCode: string organizationId: string name: string - alias: string - type: string - entityId: string + alias?: string + type?: string + entityId?: string parentAccountId: string - portfolioId: string + portfolioId?: string | null + portfolio: Pick portfolioName?: string productId: string status: { code: string description: string } - metadata: Record + metadata?: Record createdAt: Date updatedAt: Date deletedAt: Date | null diff --git a/src/types/portfolio-type.ts b/src/types/portfolio-type.ts new file mode 100644 index 00000000..4fd10b86 --- /dev/null +++ b/src/types/portfolio-type.ts @@ -0,0 +1,4 @@ +export type PortfolioType = { + id: string + name: string +} diff --git a/tailwind.config.ts b/tailwind.config.ts index d4d9ba6e..2cb799ef 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -29,10 +29,6 @@ const config = { entityBox: '0px 10px 20px rgba(0, 0, 0, 0.05), 0px 1px 2px rgba(0, 0, 0, 0.10)' }, - backgroundImage: { - 'login-wallpaper': "url('/images/login-wallpaper.jpg')", - 'loading-wallpaper': "url('/images/loading-wallpaper.jpg')" - }, colors: { sunglow: { '50': '#fefbe8',