ac2c794542
TransactionsTable — server-driven история транзакций (GET /api/billing/transactions, табы → фильтр type). InvoicesTable — GET /api/billing/invoices с empty-state (real-but-empty до Б-1). billingFormatters почищен (drop status/format-функций), mockBilling ужат до pending-баннера (E4). Sprint 2 Plan C, audit E3 (frontend pt2). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
150 lines
3.9 KiB
Vue
150 lines
3.9 KiB
Vue
<script setup lang="ts">
|
|
/**
|
|
* InvoicesTable — список счетов тенанта. Данные — GET /api/billing/invoices
|
|
* (E3). Real-but-empty до Б-1: на MVP saas_invoices пуста (нужно
|
|
* зарегистрированное юр-лицо), компонент показывает empty-state.
|
|
*/
|
|
import { ref, onMounted } from 'vue';
|
|
import { getInvoices, type BillingInvoice } from '../../api/billing';
|
|
import { formatPlain } from '../../composables/billingFormatters';
|
|
|
|
const invoices = ref<BillingInvoice[]>([]);
|
|
const loading = ref(true);
|
|
const loadError = ref<string | null>(null);
|
|
|
|
const STATUS_LABELS: Record<string, string> = {
|
|
draft: 'Черновик',
|
|
issued: 'Выставлен',
|
|
paid: 'Оплачен',
|
|
overdue: 'Просрочен',
|
|
cancelled: 'Отменён',
|
|
};
|
|
|
|
function statusLabel(status: string): string {
|
|
return STATUS_LABELS[status] ?? status;
|
|
}
|
|
|
|
function formatDate(iso: string): string {
|
|
return new Date(iso).toLocaleDateString('ru-RU', { timeZone: 'Europe/Moscow' });
|
|
}
|
|
|
|
async function load(): Promise<void> {
|
|
loading.value = true;
|
|
loadError.value = null;
|
|
try {
|
|
invoices.value = (await getInvoices()).data;
|
|
} catch {
|
|
loadError.value = 'Не удалось загрузить счета.';
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
onMounted(load);
|
|
|
|
defineExpose({ load, invoices });
|
|
</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>
|
|
</div>
|
|
<v-divider />
|
|
|
|
<div v-if="loading" class="py-8 d-flex justify-center">
|
|
<v-progress-circular indeterminate color="primary" size="28" />
|
|
</div>
|
|
|
|
<v-alert v-else-if="loadError" type="error" variant="tonal" density="compact" class="ma-4" role="alert">
|
|
{{ loadError }}
|
|
</v-alert>
|
|
|
|
<div v-else-if="invoices.length === 0" class="empty pa-8 text-center text-medium-emphasis">
|
|
Счета появятся после первой оплаты.
|
|
</div>
|
|
|
|
<ul v-else class="invoices-list pa-2 ma-0">
|
|
<li v-for="inv in invoices" :key="inv.id" class="inv-row">
|
|
<span class="inv-when num">{{ formatDate(inv.issued_at) }}</span>
|
|
<span class="inv-name">
|
|
{{ inv.invoice_number }}
|
|
<span class="sub">{{ statusLabel(inv.status) }}</span>
|
|
</span>
|
|
<span class="inv-amount num">{{ formatPlain(Number(inv.amount_total)) }}</span>
|
|
<v-btn
|
|
variant="text"
|
|
size="small"
|
|
prepend-icon="mdi-file-pdf-box"
|
|
:disabled="!inv.has_pdf"
|
|
>
|
|
PDF
|
|
</v-btn>
|
|
</li>
|
|
</ul>
|
|
</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;
|
|
}
|
|
|
|
.empty {
|
|
font-size: 14px;
|
|
}
|
|
|
|
.invoices-list {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
}
|
|
.inv-row {
|
|
display: grid;
|
|
grid-template-columns: 110px 1fr auto auto;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 12px 16px;
|
|
border-bottom: 1px solid #f0ede4;
|
|
}
|
|
.inv-row:last-child {
|
|
border-bottom: none;
|
|
}
|
|
.inv-when {
|
|
font-size: 12px;
|
|
color: #66635c;
|
|
}
|
|
.inv-name {
|
|
display: flex;
|
|
flex-direction: column;
|
|
font-weight: 500;
|
|
color: #081319;
|
|
}
|
|
.inv-name .sub {
|
|
font-weight: 400;
|
|
color: #66635c;
|
|
font-size: 12px;
|
|
}
|
|
.inv-amount {
|
|
font-weight: 500;
|
|
color: #081319;
|
|
}
|
|
</style>
|