c5242271d7
Closes Audit #3 P3 batch. Changes: 1. **knip.config.ts cleanup** — remove 4 stale config hints flagged in Audit #3 Phase 1B (`ignore: tests/**` redundant since `project` is `resources/js/**`; `ignoreDependencies` for vitest/@vue/test-utils/jsdom redundant since knip auto-detects test frameworks). Add `histoire.config.ts` + `resources/js/histoire.setup.ts` to entry — closes 2 documented FPs (histoire.setup.ts + @histoire/plugin-vue unused-flag). Verified: `npx knip` exits 0 clean. 2. **Admin table actions column header label** — change `title: ''` → `title: 'Действия'` in: - TenantsTable.vue (actions column, /admin/tenants) - AdminSupplierPricesView.vue (actions column, /admin/supplier-prices) Closes axe-core `empty-table-header` violation seen in Audit #3 Phase 7 on /admin/tenants. Header is now visible in UI (better UX than sr-only sleight-of-hand). 3. **npm overrides for lodash** in `package.json` — pin `pa11y-ci > lodash` to ^4.17.21. Verified: `npm ls lodash` resolves to lodash@4.17.23 (latest 4.x; CVE-2021-23337 + GHSA-f23m patched in <4.17.21, our version is above that). npm audit may still surface advisory ranges as informational. 4. **Decision doc for pgFormatter (Q.HARD.002)** — explicit FIX-DEFER with 3-hypothesis comparison (Strawberry Perl install vs sqlfluff replacement vs Docker pg_format vs drop SQL formatting). Decision: drop automated SQL formatting until Б-1 closure; squawk (linter) covers correctness. Addendum: axe-core .v-overlay-container region landmark — no permanent axe-core test setup exists, so no whitelist needed at this point. Verification: - knip: 0 issues - vue-tsc: 0 errors - ESLint: 0 errors - Vitest: 91 files / 736 passed / 3 skipped (no regressions) - Vite build: 2.03s Plan: docs/superpowers/plans/2026-05-14-audit3-deferred-fixes.md Task 4. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
122 lines
4.7 KiB
Vue
122 lines
4.7 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];
|
||
}>();
|
||
|
||
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: 56 },
|
||
]"
|
||
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 }}</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="Войти как клиент (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>
|