5f209a2fcc
Раунд 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>
139 lines
5.6 KiB
Vue
139 lines
5.6 KiB
Vue
<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>
|