182 lines
6.6 KiB
TypeScript
182 lines
6.6 KiB
TypeScript
import type {
|
|
AdminTenant as ApiAdminTenant,
|
|
AdminTenantDetailResponse,
|
|
ApiTenantActivityItem,
|
|
ApiTenantAliveness,
|
|
ApiTenantBalanceTx,
|
|
ApiTenantProject,
|
|
ApiTenantUser,
|
|
} from '../api/admin';
|
|
import type {
|
|
AdminTenantDetail,
|
|
TenantActivityItem,
|
|
TenantAliveness,
|
|
TenantBalanceTx,
|
|
TenantProject,
|
|
TenantUser,
|
|
} from './mockTenantDetail';
|
|
import type { TenantStatus, TenantTariff } from './mockTenants';
|
|
|
|
/**
|
|
* Маппит AdminTenantDetailResponse (API) → AdminTenantDetail (UI mock-формат).
|
|
*
|
|
* Поля отсутствующие в API/schema (inn, contact_phone, legal_address, role) —
|
|
* подставляются пустыми/'—'/'manager'-default'ом. На MVP UI показывает «—»
|
|
* через v-if; в Post-MVP добавим legal_entities-связку.
|
|
*
|
|
* `code` ← tenant.subdomain (естественный URL slug).
|
|
*/
|
|
|
|
const TARIFF_FALLBACK: TenantTariff = 'Trial';
|
|
const KNOWN_TARIFFS: TenantTariff[] = ['Trial', 'Start', 'Команда', 'Pro', 'Enterprise'];
|
|
|
|
function deriveStatus(api: ApiAdminTenant): TenantStatus {
|
|
const balance = parseFloat(api.balance_rub);
|
|
const chargeback = parseFloat(api.chargeback_unrecovered_rub);
|
|
if (api.is_trial) return 'trial';
|
|
if (api.status === 'suspended') return 'suspended';
|
|
if (chargeback > 0 || balance < 0) return 'overdue';
|
|
if (api.status === 'active') return 'active';
|
|
return 'suspended';
|
|
}
|
|
|
|
function statusText(status: TenantStatus): string {
|
|
const map: Record<TenantStatus, string> = {
|
|
active: 'Активен',
|
|
trial: 'Trial',
|
|
overdue: 'Просрочка',
|
|
suspended: 'Приостановлен',
|
|
};
|
|
return map[status];
|
|
}
|
|
|
|
function deriveTariff(name: string | null, isTrial: boolean): TenantTariff {
|
|
if (isTrial) return 'Trial';
|
|
if (name === null) return TARIFF_FALLBACK;
|
|
const match = KNOWN_TARIFFS.find((t) => t === name);
|
|
return match ?? TARIFF_FALLBACK;
|
|
}
|
|
|
|
function activitySinceText(lastActivityIso: string | null, now: Date): string {
|
|
if (lastActivityIso === null) return 'не активен';
|
|
const minutesAgo = Math.floor((now.getTime() - new Date(lastActivityIso).getTime()) / 60_000);
|
|
if (minutesAgo < 1) return 'только что';
|
|
if (minutesAgo < 60) return `${minutesAgo} мин назад`;
|
|
if (minutesAgo < 60 * 24) return `${Math.floor(minutesAgo / 60)} ч назад`;
|
|
return `${Math.floor(minutesAgo / (60 * 24))} д назад`;
|
|
}
|
|
|
|
function mapUser(u: ApiTenantUser): TenantUser {
|
|
const fullName = [u.first_name, u.last_name].filter(Boolean).join(' ').trim();
|
|
return {
|
|
id: u.id,
|
|
email: u.email,
|
|
fullName: fullName !== '' ? fullName : u.email,
|
|
// role в schema users отсутствует — на MVP единая роль 'manager'.
|
|
// Расширение в legal_entities/user_roles вынесено в Post-MVP.
|
|
role: 'manager',
|
|
// users.last_active_at не заполняется никем (замерено 07.08.2026 —
|
|
// ноль из одиннадцати), поэтому показываем живой last_login_at.
|
|
last_login_at: u.last_login_at ?? '',
|
|
is_active: u.is_active,
|
|
};
|
|
}
|
|
|
|
function mapProject(p: ApiTenantProject): TenantProject {
|
|
return {
|
|
id: p.id,
|
|
name: p.name,
|
|
slug: p.tag ?? '',
|
|
suppliers: p.suppliers_count,
|
|
leadsToday: p.leads_today,
|
|
desiredToday: p.daily_limit_target,
|
|
is_active: p.is_active,
|
|
};
|
|
}
|
|
|
|
const TX_TYPE_MAP: Record<string, TenantBalanceTx['type']> = {
|
|
topup: 'topup',
|
|
lead_charge: 'lead_charge',
|
|
refund: 'refund',
|
|
manual_adjustment: 'manual_adjustment',
|
|
// schema-only типы → ближайший UI-эквивалент.
|
|
chargeback_writedown: 'manual_adjustment',
|
|
chargeback_repayment: 'topup',
|
|
trial_bonus: 'topup',
|
|
historical_import: 'manual_adjustment',
|
|
};
|
|
|
|
function mapBalanceTx(tx: ApiTenantBalanceTx): TenantBalanceTx {
|
|
return {
|
|
id: `TX-${tx.id}`,
|
|
type: TX_TYPE_MAP[tx.type] ?? 'manual_adjustment',
|
|
amount: parseFloat(tx.amount_rub),
|
|
description: tx.description ?? '',
|
|
created_at: tx.created_at,
|
|
};
|
|
}
|
|
|
|
function mapAliveness(a: ApiTenantAliveness): TenantAliveness {
|
|
return {
|
|
state: a.state,
|
|
lastLoginAt: a.last_login_at,
|
|
daysSinceLogin: a.days_since_login,
|
|
lastLoginIp: a.last_login_ip,
|
|
lastLoginUserAgent: a.last_login_user_agent,
|
|
days: a.days,
|
|
visits30d: a.counters.visits_30d,
|
|
activeDays30d: a.counters.active_days_30d,
|
|
actions30d: a.counters.actions_30d,
|
|
failedLogins30d: a.counters.failed_logins_30d,
|
|
};
|
|
}
|
|
|
|
export function mapTenantActivityItem(i: ApiTenantActivityItem): TenantActivityItem {
|
|
return {
|
|
id: i.id,
|
|
at: i.at,
|
|
kind: i.kind,
|
|
event: i.event,
|
|
actor: i.actor,
|
|
subject: i.subject,
|
|
amountRub: i.amount_rub !== null ? parseFloat(i.amount_rub) : null,
|
|
bulkCount: i.bulk_count,
|
|
};
|
|
}
|
|
|
|
export function mapAdminTenantDetail(api: AdminTenantDetailResponse, now: Date = new Date()): AdminTenantDetail {
|
|
const status = deriveStatus(api.tenant);
|
|
const balanceRub = parseFloat(api.tenant.balance_rub);
|
|
return {
|
|
// base AdminTenant
|
|
id: api.tenant.id,
|
|
code: api.tenant.subdomain,
|
|
name: api.tenant.organization_name,
|
|
inn: '',
|
|
status,
|
|
statusText: statusText(status),
|
|
tariff: deriveTariff(api.tenant.tariff_name, api.tenant.is_trial),
|
|
balanceRub,
|
|
todayDesired: api.tenant.desired_daily_numbers ?? 0,
|
|
todayActual: api.metrics.leads_today,
|
|
mrrRub: api.tenant.mrr_rub !== null ? parseFloat(api.tenant.mrr_rub) : null,
|
|
activitySince: activitySinceText(api.tenant.last_activity_at, now),
|
|
alivenessState: api.aliveness.state,
|
|
// расширение AdminTenantDetail
|
|
contact_email: api.tenant.contact_email,
|
|
contact_phone: '',
|
|
legal_address: '',
|
|
created_at: api.tenant.created_at ?? '',
|
|
users: api.users.map(mapUser),
|
|
projects: api.projects.map(mapProject),
|
|
balanceHistory: api.balance_history.map(mapBalanceTx),
|
|
aliveness: mapAliveness(api.aliveness),
|
|
activity: api.activity.items.map(mapTenantActivityItem),
|
|
activityHasMore: api.activity.has_more,
|
|
leadsThisMonth: api.metrics.leads_this_month,
|
|
leadsThisWeek: api.metrics.leads_this_week,
|
|
avgLeadCost: api.metrics.avg_lead_cost_rub ?? 0,
|
|
runwayDays: api.metrics.runway_days ?? 0,
|
|
};
|
|
}
|