This commit is contained in:
Nadar
2026-03-17 13:24:22 +03:00
commit 82e5ac9d81
554 changed files with 29637 additions and 0 deletions

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>