Files
portal/app/resources/js/components/admin/tenants/TenantsTable.vue
T
Дмитрий 5f209a2fcc fix(ui): косметика UI-аудита — даты дд.мм.гггг, инлайн-валидация, формат денег, aria, тосты, статус-метки, админка
Раунд 2 минор-фиксы (Playwright-аудит):
- RuDateField (новый): даты дд.мм.гггг через ru date-picker вместо нативного
  <input type=date> (показывал мм/дд/гггг на en-локали) — Отчёты + Сделки.
- BalanceCapacityIndicator: разделитель тысяч «1 000 ₽», эмодзи→mdi.
- dealsApiMapper/DealDetailBody: статус-смена в активности русскими метками
  (было «viewed → new» сырыми слагами).
- ProfileTab: инлайн-валидация Имя/Фамилия (под полем, как в Реквизитах).
- RequisitesTab: проверка формата телефона на клиенте.
- ApiTab: eye-toggle с aria-label (показать/скрыть ключ и секрет).
- DashboardView: «3 / 0» → скрываем «/ N» и «лимит тарифа» при лимите 0.
- KanbanView: тост-подтверждение при смене статуса (+ цветной фейл-тост).
- NotificationsTab: убран жаргон «users.notification_preferences в БД».
- Админка: TenantsTable «ИНН не указан» вместо пустого «ИНН »; PricingTiers
  epoch-дата «1970»→«начала» + ru-формат цены; Incidents empty-state «Инцидентов
  нет»; SupplierIntegration/PdSubjectRequests — window.confirm/alert → v-dialog/snackbar.

Верификация: type-check, build, Playwright (даты дд.мм.гггг подтверждены).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 17:08:51 +03:00

139 lines
5.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import type { AdminTenant, TenantStatus } from '../../../composables/mockTenants';
defineProps<{
tenants: AdminTenant[];
}>();
const emit = defineEmits<{
rowClick: [tenant: AdminTenant];
impersonate: [tenant: AdminTenant];
editBalance: [tenant: AdminTenant];
}>();
function formatRub(v: number): string {
return new Intl.NumberFormat('ru-RU').format(v) + ' ₽';
}
function formatBalance(v: number): string {
if (v === 0) return '0';
if (v < 0) return '' + new Intl.NumberFormat('ru-RU').format(Math.abs(v));
return new Intl.NumberFormat('ru-RU').format(v);
}
function statusColor(s: TenantStatus): string {
if (s === 'active') return 'success';
if (s === 'trial') return 'info';
if (s === 'overdue') return 'warning';
return 'error';
}
</script>
<template>
<v-card variant="outlined" class="mt-4 panel">
<v-data-table
:items="tenants"
:headers="[
{ title: 'Тенант', key: 'name', sortable: false },
{ title: 'Статус', key: 'status', sortable: false },
{ title: 'Тариф', key: 'tariff', sortable: false },
{ title: 'Баланс ₽', key: 'balanceRub', align: 'end', sortable: false },
{ title: 'Желаем×факт сегодня', key: 'today', align: 'end', sortable: false },
{ title: 'MRR', key: 'mrrRub', align: 'end', sortable: false },
{ title: 'Активность', key: 'activitySince', sortable: false },
{ title: 'Действия', key: 'actions', align: 'end', sortable: false, width: 96 },
]"
items-per-page="-1"
hide-default-footer
hover
density="comfortable"
@click:row="(_e: Event, { item }: { item: AdminTenant }) => emit('rowClick', item)"
>
<template #[`item.name`]="{ item }: { item: AdminTenant }">
<div class="cell-tenant">
<div class="t-name">{{ item.name }}</div>
<div class="t-inn text-caption text-medium-emphasis">
{{ item.inn ? `ИНН ${item.inn}` : 'ИНН не указан' }}
</div>
</div>
</template>
<template #[`item.status`]="{ item }: { item: AdminTenant }">
<v-chip size="small" variant="tonal" :color="statusColor(item.status)">
{{ item.statusText }}
</v-chip>
</template>
<template #[`item.balanceRub`]="{ item }: { item: AdminTenant }">
<span
class="num"
:class="{ 'text-error': item.balanceRub < 0, 'text-medium-emphasis': item.balanceRub === 0 }"
>
{{ formatBalance(item.balanceRub) }}
</span>
</template>
<template #[`item.today`]="{ item }: { item: AdminTenant }">
<span class="num">{{ item.todayDesired }} × {{ item.todayActual }}</span>
</template>
<template #[`item.mrrRub`]="{ item }: { item: AdminTenant }">
<span v-if="item.mrrRub !== null" class="num">{{ formatRub(item.mrrRub) }}</span>
<span v-else class="text-medium-emphasis"></span>
</template>
<template #[`item.activitySince`]="{ item }: { item: AdminTenant }">
<span class="num text-medium-emphasis">{{ item.activitySince }}</span>
</template>
<template #[`item.actions`]="{ item }: { item: AdminTenant }">
<v-tooltip text="Изменить баланс" location="top" aria-label="Изменить баланс">
<template #activator="{ props: tipProps }">
<v-btn
v-bind="tipProps"
icon="mdi-cash-edit"
variant="text"
size="small"
density="comfortable"
:aria-label="`Изменить баланс для ${item.name}`"
:data-testid="`edit-balance-btn-${item.id}`"
@click.stop="emit('editBalance', item)"
/>
</template>
</v-tooltip>
<v-tooltip
text="Войти как клиент (impersonation)"
location="top"
aria-label="Войти как клиент (impersonation)"
>
<template #activator="{ props: tipProps }">
<v-btn
v-bind="tipProps"
icon="mdi-account-switch"
variant="text"
size="small"
density="comfortable"
:aria-label="`Войти как клиент (impersonation) для ${item.name}`"
:disabled="item.status === 'suspended'"
:data-testid="`impersonate-btn-${item.id}`"
@click.stop="emit('impersonate', item)"
/>
</template>
</v-tooltip>
</template>
</v-data-table>
</v-card>
</template>
<style scoped>
.panel {
background: #fff;
}
.num {
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-feature-settings: 'tnum';
font-weight: 500;
}
.cell-tenant {
padding: 4px 0;
}
.t-name {
font-weight: 500;
color: #081319;
}
</style>