Files
portal/app/resources/js/components/billing/TransactionsTable.vue
T

190 lines
5.3 KiB
Vue

<script setup lang="ts">
/**
* TransactionsTable — server-driven история транзакций с табами
* (Все / Пополнения / Списания / Возвраты). Данные — GET
* /api/billing/transactions (E3). Паттерн self-fetching из ChargesTab.
*/
import { ref, onMounted } from 'vue';
import { getTransactions, type BillingTransaction } from '../../api/billing';
import { formatCost, txAmountClass } from '../../composables/billingFormatters';
interface Tab {
id: string;
label: string;
type: string | null;
}
const TABS: Tab[] = [
{ id: 'all', label: 'Все', type: null },
{ id: 'topup', label: 'Пополнения', type: 'topup' },
{ id: 'lead_charge', label: 'Списания', type: 'lead_charge' },
];
const activeTab = ref<string>('all');
const rows = ref<BillingTransaction[]>([]);
const total = ref(0);
const loading = ref(false);
const loadError = ref<string | null>(null);
const page = ref(1);
const headers = [
{ title: 'Дата', key: 'created_at', sortable: false },
{ title: 'Операция', key: 'description', sortable: false },
{ title: 'ID', key: 'code', sortable: false, width: 120 },
{ title: 'Сумма', key: 'amount_rub', align: 'end' as const, sortable: false, width: 140 },
];
function formatWhen(iso: string): string {
return new Date(iso).toLocaleString('ru-RU', {
timeZone: 'Europe/Moscow',
year: '2-digit',
day: '2-digit',
month: '2-digit',
hour: '2-digit',
minute: '2-digit',
});
}
/** Числовое значение движения из display_amount_rub. */
function txAmountValue(tx: BillingTransaction): number {
return Number(tx.display_amount_rub);
}
/** Текст суммы из display_amount_rub. */
function txAmountText(tx: BillingTransaction): string {
return formatCost(Number(tx.display_amount_rub));
}
async function load(): Promise<void> {
loading.value = true;
loadError.value = null;
try {
const tab = TABS.find((t) => t.id === activeTab.value);
const params: { page: number; type?: string } = { page: page.value };
if (tab?.type) params.type = tab.type;
const res = await getTransactions(params);
rows.value = res.data;
total.value = res.meta.total;
} catch {
loadError.value = 'Не удалось загрузить транзакции.';
rows.value = [];
total.value = 0;
} finally {
loading.value = false;
}
}
async function changeTab(id: string): Promise<void> {
activeTab.value = id;
page.value = 1;
await load();
}
async function loadOptions(opts: { page: number }): Promise<void> {
page.value = opts.page;
await load();
}
async function refresh(): Promise<void> {
page.value = 1;
await load();
}
onMounted(load);
defineExpose({ load, refresh, changeTab, activeTab, total, rows });
</script>
<template>
<v-card variant="outlined" class="mt-4 panel">
<div class="panel-h pa-4">
<h2 class="text-h6 panel-title ma-0">История транзакций</h2>
<v-btn-toggle
:model-value="activeTab"
mandatory
color="primary"
density="comfortable"
variant="text"
>
<v-btn
v-for="tab in TABS"
:key="tab.id"
:value="tab.id"
size="small"
@click="changeTab(tab.id)"
>
{{ tab.label }}
</v-btn>
</v-btn-toggle>
</div>
<v-alert v-if="loadError" type="error" variant="tonal" density="compact" class="mx-4 mb-4" role="alert">
{{ loadError }}
</v-alert>
<v-data-table-server
:headers="headers"
:items="rows"
:items-length="total"
:loading="loading"
:items-per-page="20"
density="comfortable"
@update:options="loadOptions"
>
<template #[`item.created_at`]="{ item }">
<span class="tx-when num">{{ formatWhen(item.created_at) }}</span>
</template>
<template #[`item.code`]="{ item }">
<span class="tx-id">#{{ item.code }}</span>
</template>
<template #[`item.amount_rub`]="{ item }">
<span class="num" :class="txAmountClass(txAmountValue(item))">
{{ txAmountText(item) }}
</span>
</template>
</v-data-table-server>
</v-card>
</template>
<style scoped>
.num {
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-feature-settings: 'tnum';
font-weight: 500;
}
.panel {
background: #fff;
}
.panel-h {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 12px;
}
.panel-title {
font-variation-settings: 'opsz' 18;
letter-spacing: -0.01em;
}
.tx-when {
font-size: 12px;
color: #66635c;
}
.tx-id {
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-size: 12px;
color: #66635c;
}
.tx-amount-up {
color: #1b6e3b;
}
.tx-amount-down {
color: #b83a3a;
}
.tx-amount-neutral {
color: #66635c;
}
</style>