initial
This commit is contained in:
97
apps/client/pages/projects/[projectId].vue
Normal file
97
apps/client/pages/projects/[projectId].vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<PageHeader
|
||||
:title="project.name"
|
||||
with-back-button
|
||||
>
|
||||
<div class="project-header">
|
||||
<MoneyAmount
|
||||
class="project-header__balance"
|
||||
:value="account.balance"
|
||||
currency="USDT"
|
||||
/>
|
||||
|
||||
<UiButton
|
||||
icon="s-restore"
|
||||
size="small"
|
||||
type="outlined"
|
||||
color="secondary"
|
||||
:loading="accountPending"
|
||||
@click="accountRefresh()"
|
||||
/>
|
||||
</div>
|
||||
</PageHeader>
|
||||
|
||||
<UiTabs v-model="activeTab" class="mb-16" :options="tabs" />
|
||||
|
||||
<NuxtPage />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
middleware: ['auth'],
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const activeTab = ref(route.path)
|
||||
|
||||
const { data: project } = await useAsyncData('project', () => {
|
||||
return $api(`/online_stores/${route.params.projectId}`, {
|
||||
method: 'get',
|
||||
})
|
||||
})
|
||||
|
||||
if (!project.value) {
|
||||
throw createError({
|
||||
statusCode: 404,
|
||||
statusMessage: 'Page Not Found',
|
||||
})
|
||||
}
|
||||
|
||||
const {
|
||||
data: account,
|
||||
refresh: accountRefresh,
|
||||
pending: accountPending,
|
||||
} = await useAsyncData('account', () =>
|
||||
$api(`/accounts/${project.value.accountIds[0]}`, {
|
||||
method: 'get',
|
||||
}))
|
||||
|
||||
const intervalId = setInterval(() => refreshNuxtData(['invoices', 'transactions', 'account']), 30000)
|
||||
|
||||
const tabs = computed(() => [
|
||||
{
|
||||
label: 'Assets',
|
||||
value: `/projects/${route.params.projectId}`,
|
||||
},
|
||||
{
|
||||
label: 'Invoices',
|
||||
value: `/projects/${route.params.projectId}/invoices`,
|
||||
},
|
||||
{
|
||||
label: 'Transactions',
|
||||
value: `/projects/${route.params.projectId}/transactions`,
|
||||
},
|
||||
])
|
||||
|
||||
watch(activeTab, (value) => {
|
||||
router.push(value)
|
||||
})
|
||||
|
||||
onUnmounted(() => clearInterval(intervalId))
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.project-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
&__balance {
|
||||
@include h2('money-amount-value', true);
|
||||
|
||||
color: $clr-grey-500;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
110
apps/client/pages/projects/[projectId]/index.vue
Normal file
110
apps/client/pages/projects/[projectId]/index.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<PageToolbar
|
||||
v-model:search="searchTerm"
|
||||
search-label="Search an asset"
|
||||
>
|
||||
<UiButton
|
||||
size="large"
|
||||
@click="notify({ id: 'wip', type: 'warning', text: 'Work in progress' })"
|
||||
>
|
||||
Manage assets
|
||||
</UiButton>
|
||||
</PageToolbar>
|
||||
|
||||
<div class="asset-list">
|
||||
<AssetCard
|
||||
v-for="asset in filteredAssets"
|
||||
:key="asset.code"
|
||||
v-bind="asset"
|
||||
:is-active="sidebarOptions.modelValue && sidebarOptions.attrs.asset?.code === asset.code"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useModal } from 'vue-final-modal'
|
||||
import { AssetSidebar } from '#components'
|
||||
|
||||
definePageMeta({
|
||||
middleware: ['auth'],
|
||||
})
|
||||
|
||||
const { data: account } = useNuxtData('account')
|
||||
|
||||
const route = useRoute()
|
||||
const notify = useNotify()
|
||||
const { open, patchOptions, options: sidebarOptions } = useModal({
|
||||
component: AssetSidebar,
|
||||
attrs: {
|
||||
projectId: route.params.projectId,
|
||||
},
|
||||
})
|
||||
|
||||
const searchTerm = ref('')
|
||||
|
||||
const assets = shallowRef([
|
||||
'USDT:Tether',
|
||||
'LTC:Litecoin',
|
||||
'DOGE:Dogecoin',
|
||||
'BTC:Bitcoin',
|
||||
'XRP:Ripple',
|
||||
'BNB:Binance',
|
||||
'BAT:Basic Attention Token',
|
||||
'EOS:EOS Network',
|
||||
'MKR:Maker',
|
||||
'DAI:Dai',
|
||||
'ETH:Ethereum',
|
||||
'DASH:Dash',
|
||||
].map((item, idx) => {
|
||||
const [code, name] = item.split(':')
|
||||
const balance = $money.format(code === 'USDT' ? account.value.balance : 0, code)
|
||||
const rate = code === 'USDT' ? 1 : 213.25
|
||||
const withdraw = undefined
|
||||
// const balance = $money.format(code === 'USDT' ? account.value.balance : 0.002741, code)
|
||||
// const withdraw = idx % 2 !== 0 ? $money.format(0.15484, code) : undefined
|
||||
const disabled = code !== 'USDT'
|
||||
|
||||
return {
|
||||
code,
|
||||
name,
|
||||
balance,
|
||||
rate,
|
||||
withdraw,
|
||||
disabled,
|
||||
onClick: () => {
|
||||
if (!disabled) {
|
||||
patchOptions({
|
||||
attrs: {
|
||||
asset: {
|
||||
code,
|
||||
name,
|
||||
balance,
|
||||
rate,
|
||||
withdraw,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
open()
|
||||
}
|
||||
// else {
|
||||
// notify({ type: 'warning', title: 'Work in progress', text: 'At the moment we only work with USDT', id: 'wip' })
|
||||
// }
|
||||
},
|
||||
}
|
||||
}))
|
||||
|
||||
const { result: filteredAssets } = useFuseSearch(assets, { keys: ['code', 'name'] }, searchTerm)
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.asset-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
background-color: $clr-white;
|
||||
margin-top: 24px;
|
||||
}
|
||||
</style>
|
||||
288
apps/client/pages/projects/[projectId]/invoices.vue
Normal file
288
apps/client/pages/projects/[projectId]/invoices.vue
Normal file
@@ -0,0 +1,288 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="!total && isFiltersEmpty && !searchTerm && !isInvoicesFetching"
|
||||
class="invoice__empty"
|
||||
>
|
||||
<InvoicesEmpty :id="$route.params.projectId" />
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<PageToolbar
|
||||
v-model:search="searchTerm"
|
||||
search-label="Search an invoices"
|
||||
>
|
||||
<template #prefix>
|
||||
<div class="invoices-awaiting-sum">
|
||||
<div class="invoices-awaiting-sum__value">
|
||||
<span>{{ awaitingSum }}</span> <UiIconSUpRight class="text-clr-cyan-500" />
|
||||
</div>
|
||||
|
||||
<p class="invoices-awaiting-sum__text">
|
||||
Sum of invoices awaiting payment
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<UiButton
|
||||
type="outlined"
|
||||
color="secondary"
|
||||
size="large"
|
||||
:href="`/report/${$route.params.projectId}`"
|
||||
icon="csv"
|
||||
>
|
||||
Export to CSV
|
||||
</UiButton>
|
||||
|
||||
<UiButton
|
||||
size="large"
|
||||
:href="`/create/invoice/${$route.params.projectId}`"
|
||||
>
|
||||
Create an invoice
|
||||
</UiButton>
|
||||
</PageToolbar>
|
||||
|
||||
<div
|
||||
style="
|
||||
margin-top: 24px;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
background-color: #fff;
|
||||
"
|
||||
>
|
||||
<ResourceFilters
|
||||
class="mb-16"
|
||||
:filters="filters"
|
||||
/>
|
||||
|
||||
<StrippedTable
|
||||
class="invoices__table"
|
||||
:columns="columns"
|
||||
:data="invoices"
|
||||
:loading="isInvoicesFetching"
|
||||
>
|
||||
<template #cell(currency)="{ row: { original } }">
|
||||
<CurrencyName :code="original.currencyCode" />
|
||||
</template>
|
||||
|
||||
<template #cell(amount)="{ row: { original } }">
|
||||
<MoneyAmount
|
||||
:value="original.amount"
|
||||
:currency="original.currencyCode"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #cell(accrued)="{ row: { original } }">
|
||||
<MoneyAmount
|
||||
:value="original.accrued"
|
||||
:currency="original.currencyCode"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #cell(address)="{ row: { original } }">
|
||||
<span v-if="!original.walletAddress">—</span>
|
||||
<TextShortener
|
||||
v-else
|
||||
:text="original.walletAddress"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #cell(orderId)="{ row: { original } }">
|
||||
<TextShortener :text="original.orderId" />
|
||||
</template>
|
||||
|
||||
<template #cell(date)="{ row: { original } }">
|
||||
<FormattedDate :value="original.createdAt" />
|
||||
</template>
|
||||
|
||||
<template #cell(status)="{ row: { original } }">
|
||||
<UiBadge
|
||||
:text="$t(`invoice_status.${original.status}`)"
|
||||
:type="getStatusType(original.status)"
|
||||
:icon="getStatusIcon(original.status)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #cell(paymentLink)="{ row: { original } }">
|
||||
<UiCopyButton
|
||||
title="Payment link"
|
||||
pressed-title="Link copied"
|
||||
:value="`${runtimeConfig.public.payHost}/${original.id}`"
|
||||
/>
|
||||
</template>
|
||||
</StrippedTable>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { createColumnHelper } from '@tanstack/vue-table'
|
||||
import { computed } from 'vue'
|
||||
import dayjs from 'dayjs'
|
||||
import { getStatusIcon, getStatusType } from '~/helpers/invoices.ts'
|
||||
|
||||
definePageMeta({
|
||||
middleware: ['auth'],
|
||||
})
|
||||
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
|
||||
const filters = [
|
||||
{
|
||||
key: 'statuses[]',
|
||||
label: 'Status',
|
||||
placeholder: 'All statuses',
|
||||
multiple: true,
|
||||
options: [
|
||||
{
|
||||
label: 'Pending',
|
||||
value: 'pending',
|
||||
},
|
||||
{
|
||||
label: 'Completed',
|
||||
value: 'completed',
|
||||
},
|
||||
{
|
||||
label: 'Expired',
|
||||
value: 'expired',
|
||||
},
|
||||
{
|
||||
label: 'Await Payment',
|
||||
value: 'awaiting_payment',
|
||||
},
|
||||
{
|
||||
label: 'Overpaid',
|
||||
value: 'overpaid',
|
||||
},
|
||||
{
|
||||
label: 'Underpaid',
|
||||
value: 'underpaid',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'calendar',
|
||||
key: 'date',
|
||||
label: 'Date',
|
||||
placeholder: 'All period',
|
||||
transform: (value) => {
|
||||
const [dateFrom, dateTo] = value
|
||||
|
||||
if (!dateFrom)
|
||||
return
|
||||
|
||||
return {
|
||||
dateFrom: dayjs(dateFrom).format('YYYY-MM-DD'),
|
||||
dateTo: dateTo ? dayjs(dateTo).format('YYYY-MM-DD') : undefined,
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const route = useRoute()
|
||||
const { appliedFilters, empty: isFiltersEmpty } = useFilters(filters)
|
||||
|
||||
const searchTerm = ref()
|
||||
|
||||
const { data: invoicesData, pending: isInvoicesFetching, refresh } = await useAsyncData(
|
||||
'invoices',
|
||||
() =>
|
||||
$api(`/invoices`, {
|
||||
method: 'get',
|
||||
params: {
|
||||
...appliedFilters.value,
|
||||
onlineStoreId: route.params.projectId,
|
||||
query: searchTerm.value,
|
||||
},
|
||||
}),
|
||||
{
|
||||
watch: [appliedFilters, searchTerm],
|
||||
},
|
||||
)
|
||||
|
||||
const total = computed(() => invoicesData.value?.meta.total ?? 0)
|
||||
const invoices = computed(() => invoicesData.value?.invoices)
|
||||
|
||||
const awaitingSum = $money.fullFormat(0, 'USDT')
|
||||
|
||||
const columnHelper = createColumnHelper()
|
||||
|
||||
const columns = [
|
||||
columnHelper.accessor('currencyCode', {
|
||||
id: 'currency',
|
||||
header: 'Currency',
|
||||
}),
|
||||
columnHelper.accessor('amount', {
|
||||
header: 'Amount',
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: 'accrued',
|
||||
header: 'Accrued',
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: 'address',
|
||||
header: 'Address',
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: 'orderId',
|
||||
header: 'Order ID',
|
||||
}),
|
||||
columnHelper.accessor('createdAt', {
|
||||
id: 'date',
|
||||
header: 'Date',
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: 'status',
|
||||
header: 'Status',
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: 'paymentLink',
|
||||
}),
|
||||
]
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.invoice {
|
||||
&__empty {
|
||||
display: inline-flex;
|
||||
background-color: $clr-white;
|
||||
border-radius: 12px;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
|
||||
> :first-child {
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.invoices {
|
||||
&__table {
|
||||
td.payment_link {
|
||||
width: 1%;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.invoices-awaiting-sum {
|
||||
padding: 8px 16px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
background-color: $clr-grey-200;
|
||||
|
||||
&__value {
|
||||
@include txt-i-sb;
|
||||
|
||||
> span {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
&__text {
|
||||
@include txt-s-m;
|
||||
|
||||
color: $clr-grey-500;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
220
apps/client/pages/projects/[projectId]/transactions.vue
Normal file
220
apps/client/pages/projects/[projectId]/transactions.vue
Normal file
@@ -0,0 +1,220 @@
|
||||
<template>
|
||||
<PageToolbar
|
||||
v-model:search="searchTerm"
|
||||
search-label="Find a transaction"
|
||||
/>
|
||||
|
||||
<div
|
||||
style="
|
||||
margin-top: 24px;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
background-color: #fff;
|
||||
"
|
||||
>
|
||||
<ResourceFilters
|
||||
class="mb-16"
|
||||
:filters="filters"
|
||||
/>
|
||||
|
||||
<StrippedTable
|
||||
class="transactions__table"
|
||||
:columns="columns"
|
||||
:data="transactions"
|
||||
:loading="isTransactionsFetching"
|
||||
>
|
||||
<template #cell(currency)="{ row: { original } }">
|
||||
<CurrencyName :code="original.currencyCode" />
|
||||
</template>
|
||||
|
||||
<template #cell(amount)="{ row: { original } }">
|
||||
<MoneyAmount
|
||||
:value="original.amount"
|
||||
:currency="original.currencyCode"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #cell(accrued)="{ row: { original } }">
|
||||
<MoneyAmount
|
||||
:value="original.accrued"
|
||||
:currency="original.currencyCode"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #cell(address)="{ row: { original } }">
|
||||
<span v-if="!original.walletAddress">—</span>
|
||||
<TextShortener
|
||||
v-else
|
||||
:text="original.walletAddress"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #cell(date)="{ row: { original } }">
|
||||
<FormattedDate :value="original.createdAt" />
|
||||
</template>
|
||||
|
||||
<template #cell(type)="{ row: { original } }">
|
||||
<OperationType type="Withdraw" />
|
||||
</template>
|
||||
|
||||
<template #cell(status)="{ row: { original } }">
|
||||
<UiBadge
|
||||
:text="$t(`invoice_status.${original.status}`)"
|
||||
:type="getStatusType(original.status)"
|
||||
:icon="getStatusIcon(original.status)"
|
||||
/>
|
||||
</template>
|
||||
</StrippedTable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { createColumnHelper } from '@tanstack/vue-table'
|
||||
import dayjs from 'dayjs'
|
||||
import { getStatusIcon, getStatusType } from '~/helpers/invoices.ts'
|
||||
|
||||
definePageMeta({
|
||||
middleware: ['auth'],
|
||||
})
|
||||
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
|
||||
const filters = [
|
||||
// {
|
||||
// key: 'networks[]',
|
||||
// label: 'Networks',
|
||||
// placeholder: 'All networks',
|
||||
// multiple: true,
|
||||
// options: [],
|
||||
// },
|
||||
// {
|
||||
// key: 'assets[]',
|
||||
// label: 'Assets',
|
||||
// placeholder: 'All assets',
|
||||
// multiple: true,
|
||||
// options: [],
|
||||
// },
|
||||
{
|
||||
key: 'statuses[]',
|
||||
label: 'Status',
|
||||
placeholder: 'All statuses',
|
||||
multiple: true,
|
||||
options: [
|
||||
{
|
||||
label: 'Pending',
|
||||
value: 'pending',
|
||||
},
|
||||
{
|
||||
label: 'Completed',
|
||||
value: 'completed',
|
||||
},
|
||||
{
|
||||
label: 'Investigating',
|
||||
value: 'investigating',
|
||||
},
|
||||
{
|
||||
label: 'Broadcasted',
|
||||
value: 'broadcasted',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'calendar',
|
||||
key: 'date',
|
||||
label: 'Date',
|
||||
placeholder: 'All period',
|
||||
transform: (value) => {
|
||||
const [dateFrom, dateTo] = value
|
||||
|
||||
if (!dateFrom)
|
||||
return
|
||||
|
||||
return {
|
||||
dateFrom: dayjs(dateFrom).format('YYYY-MM-DD'),
|
||||
dateTo: dateTo ? dayjs(dateTo).format('YYYY-MM-DD') : undefined,
|
||||
}
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const route = useRoute()
|
||||
const { appliedFilters, empty: isFiltersEmpty } = useFilters(filters)
|
||||
|
||||
const searchTerm = ref()
|
||||
|
||||
const { data: transactionsData, pending: isTransactionsFetching, refresh } = await useAsyncData(
|
||||
'transactions',
|
||||
() =>
|
||||
$api(`/withdrawals`, {
|
||||
method: 'get',
|
||||
params: {
|
||||
...appliedFilters.value,
|
||||
onlineStoreId: route.params.projectId,
|
||||
query: searchTerm.value,
|
||||
},
|
||||
}),
|
||||
{
|
||||
watch: [appliedFilters, searchTerm],
|
||||
},
|
||||
)
|
||||
|
||||
const transactions = computed(() => transactionsData.value?.withdrawals)
|
||||
|
||||
const columnHelper = createColumnHelper()
|
||||
|
||||
const columns = [
|
||||
columnHelper.accessor('currencyCode', {
|
||||
id: 'currency',
|
||||
header: 'Currency',
|
||||
}),
|
||||
columnHelper.accessor('amount', {
|
||||
header: 'Amount',
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: 'accrued',
|
||||
header: 'Accrued',
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: 'address',
|
||||
header: 'Address',
|
||||
}),
|
||||
columnHelper.accessor('createdAt', {
|
||||
id: 'date',
|
||||
header: 'Date',
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: 'type',
|
||||
header: 'Type operation',
|
||||
}),
|
||||
columnHelper.display({
|
||||
id: 'status',
|
||||
header: 'Status',
|
||||
}),
|
||||
]
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.transaction {
|
||||
&__empty {
|
||||
display: inline-flex;
|
||||
background-color: $clr-white;
|
||||
border-radius: 12px;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
padding: 16px;
|
||||
|
||||
> :first-child {
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.transactions {
|
||||
&__table {
|
||||
td.payment_link {
|
||||
width: 1%;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
78
apps/client/pages/projects/create.vue
Normal file
78
apps/client/pages/projects/create.vue
Normal file
@@ -0,0 +1,78 @@
|
||||
<template>
|
||||
<PageForm
|
||||
title="Creating a project"
|
||||
back-link="/projects"
|
||||
:back-text="$t('back')"
|
||||
:submit-text="$t('create')"
|
||||
:handler="onSubmit"
|
||||
>
|
||||
<UiInput
|
||||
id="name"
|
||||
class="mb-6"
|
||||
:label="$t('field_max_characters', ['Project name', 16])"
|
||||
rules="required|max:16"
|
||||
/>
|
||||
|
||||
<UiInput
|
||||
id="description"
|
||||
class="mb-6"
|
||||
label="Description"
|
||||
rules="required"
|
||||
/>
|
||||
|
||||
<UiInput
|
||||
id="website"
|
||||
class="mb-6"
|
||||
label="Your website"
|
||||
:rules="{
|
||||
required: true,
|
||||
url: { optionalProtocol: true },
|
||||
}"
|
||||
/>
|
||||
|
||||
<UiInput
|
||||
id="callbacksUrl"
|
||||
class="mb-6"
|
||||
label="Callbacks URL"
|
||||
rules="required|url"
|
||||
/>
|
||||
|
||||
<UiInput
|
||||
id="userRedirectUrl"
|
||||
class="mb-6"
|
||||
label="User redirect URL"
|
||||
rules="required|url"
|
||||
/>
|
||||
</PageForm>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
definePageMeta({
|
||||
middleware: ['auth'],
|
||||
centerContent: true,
|
||||
})
|
||||
|
||||
const notify = useNotify()
|
||||
|
||||
async function onSubmit(values) {
|
||||
try {
|
||||
await $api('/online_stores', {
|
||||
method: 'post',
|
||||
body: values,
|
||||
})
|
||||
|
||||
notify({
|
||||
type: 'positive',
|
||||
text: 'Мерчант успешно создан',
|
||||
})
|
||||
|
||||
navigateTo('/projects')
|
||||
}
|
||||
catch (e) {
|
||||
setStaticError({
|
||||
status: e.status,
|
||||
message: 'Something went wrong',
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
137
apps/client/pages/projects/index.vue
Normal file
137
apps/client/pages/projects/index.vue
Normal file
@@ -0,0 +1,137 @@
|
||||
<template>
|
||||
<PageHeader :title="$t('projects')">
|
||||
<UiButton
|
||||
v-if="!!projects"
|
||||
icon="plus"
|
||||
href="/projects/create"
|
||||
>
|
||||
Create a new one
|
||||
</UiButton>
|
||||
|
||||
<!-- Временно убрано KITD-527 -->
|
||||
<!-- <UiButton -->
|
||||
<!-- icon="circle-question" -->
|
||||
<!-- type="outlined" -->
|
||||
<!-- :color="!!projects ? 'secondary' : 'primary'" -->
|
||||
<!-- > -->
|
||||
<!-- {{ $t('how_does_it_works') }} -->
|
||||
<!-- </UiButton> -->
|
||||
</PageHeader>
|
||||
|
||||
<div class="projects-content">
|
||||
<ProjectCreate v-if="!projects" />
|
||||
|
||||
<ProjectsTable
|
||||
v-else
|
||||
class="projects-content__table"
|
||||
:projects="projects"
|
||||
/>
|
||||
|
||||
<ProjectInfoColumns />
|
||||
</div>
|
||||
|
||||
<!-- Временно убрано KITD-527 -->
|
||||
<!-- <PageFooterInfoBlock -->
|
||||
<!-- :title="$t('test_functional')" -->
|
||||
<!-- :text="$t('learn_functional')" -->
|
||||
<!-- :action="$t('try')" -->
|
||||
<!-- /> -->
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
middleware: ['auth'],
|
||||
})
|
||||
|
||||
interface Project {
|
||||
id: number
|
||||
name: string
|
||||
account?: ProjectAccount
|
||||
}
|
||||
|
||||
interface ProjectDetailed {
|
||||
id: number
|
||||
name: string
|
||||
accountIds: number[] | string[]
|
||||
}
|
||||
|
||||
interface ProjectAccount {
|
||||
id?: number
|
||||
balance: string
|
||||
}
|
||||
|
||||
const { data: projects } = await useAsyncData('projects', async () => {
|
||||
const nuxtApp = useNuxtApp()
|
||||
|
||||
const projects = await $api<Project[]>('/online_stores', {
|
||||
method: 'get',
|
||||
})
|
||||
|
||||
if (!projects)
|
||||
return null
|
||||
|
||||
const detailedProjects = await nuxtApp.runWithContext(async () => {
|
||||
const promises = await Promise.allSettled(
|
||||
projects.map((project) => {
|
||||
return $api(`/online_stores/${project.id}`, {
|
||||
method: 'get',
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
return (
|
||||
promises.filter(
|
||||
({ status }) => status === 'fulfilled',
|
||||
) as PromiseFulfilledResult<ProjectDetailed>[]
|
||||
).map(({ value }) => value)
|
||||
})
|
||||
|
||||
const balances = await nuxtApp.runWithContext(async () => {
|
||||
const promises = await Promise.allSettled(
|
||||
detailedProjects.map((project) => {
|
||||
return $api(`/accounts/${project.accountIds[0]}`, {
|
||||
method: 'get',
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
return (
|
||||
promises.filter(
|
||||
({ status }) => status === 'fulfilled',
|
||||
) as PromiseFulfilledResult<ProjectAccount>[]
|
||||
).map(({ value }) => value)
|
||||
})
|
||||
|
||||
return projects.map((project) => {
|
||||
const detail = detailedProjects.find(detail => detail.id === project.id)
|
||||
let balance
|
||||
|
||||
if (detail)
|
||||
balance = balances.find(balance => balance.id === detail.accountIds[0])
|
||||
|
||||
balance ??= { balance: 0 } as unknown as ProjectAccount
|
||||
|
||||
return {
|
||||
...project,
|
||||
account: balance,
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.projects-content {
|
||||
display: grid;
|
||||
align-items: flex-start;
|
||||
grid-template-columns: auto 270px;
|
||||
background-color: $clr-white;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 32px;
|
||||
gap: 16px;
|
||||
|
||||
&__table {
|
||||
align-self: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user