Files
portal/app/resources/js/views/projects/NewProjectDialog.vue
T
Дмитрий fa7361364d
Accessibility (Pa11y live) / a11y (push) Has been cancelled
SAST — Semgrep / Semgrep SAST scan (push) Has been cancelled
feat: подсказка «Как увеличить количество сделок» в диалоге проекта
Над «Откуда собирать заявки» добавлена строка-подсказка с tooltip:
лимит распределяется по поставщикам равномерно; даже если не выбирается
полностью — просто увеличить лимит, и сделок придёт больше.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 13:35:29 +03:00

771 lines
35 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.
<template>
<v-dialog :model-value="modelValue" max-width="720" @update:model-value="$emit('update:modelValue', $event)">
<v-card style="position: relative">
<DevIndexBadge
:index="mode === 'edit' ? 19 : 18"
:label="mode === 'edit' ? 'EditProjectDialog' : 'NewProjectDialog'"
:dialog-mode="true"
style="top: 12px; right: 12px"
/>
<v-card-title>{{ mode === 'edit' ? 'Редактирование проекта' : 'Новый проект' }}</v-card-title>
<v-card-text>
<!-- Косяк 04 / вариант C: для нового клиента без реквизитов шаг 1
(короткие реквизиты) прямо в визарде, без ухода на /settings. -->
<div v-if="step === 'requisites'" data-testid="req-step">
<div class="steps-head mb-3">
<span class="step-on">1. Реквизиты</span>
<span class="step-sep"></span>
<span class="step-off">2. Проект</span>
</div>
<v-alert type="info" variant="tonal" density="comfortable" class="mb-4">
Сначала короткие реквизиты компании без них нельзя создать первый проект. Это пара минут.
</v-alert>
<v-select
v-model="reqForm.subject_type"
:items="subjectTypeItems"
item-title="title"
item-value="value"
label="Тип лица"
density="comfortable"
class="ld-input-quiet"
data-testid="req-subject-type"
:error-messages="reqErrors.subject_type"
/>
<v-text-field
v-model="reqForm.contact_name"
label="Контактное имя"
density="comfortable"
class="ld-input-quiet"
data-testid="req-contact-name"
:error-messages="reqErrors.contact_name"
/>
<v-text-field
v-model="reqForm.contact_phone"
label="Контактный телефон"
hint="Примем в любом формате — приведём сами"
persistent-hint
density="comfortable"
class="ld-input-quiet"
data-testid="req-contact-phone"
:error-messages="reqErrors.contact_phone"
/>
<v-text-field
v-if="reqForm.subject_type === 'legal_entity' || reqForm.subject_type === 'sole_proprietor'"
v-model="reqForm.inn"
label="ИНН"
density="comfortable"
class="ld-input-quiet mt-2"
data-testid="req-inn"
:error-messages="reqErrors.inn"
/>
<v-alert
v-if="reqGeneralError"
type="error"
variant="tonal"
density="compact"
class="mt-2"
>
{{ reqGeneralError }}
</v-alert>
</div>
<template v-else>
<!-- Баннер-объявление: когда пойдут лиды (спека 2026-06-22-project-source-edit-lock-ux). -->
<v-alert
v-if="mode !== 'edit'"
type="info"
variant="tonal"
density="comfortable"
class="mb-4"
data-testid="np-lead-banner"
>
📣 Лидерра поставит проект в сбор сразу после создания. Первые лиды пойдут с {{ leadStart }}.
</v-alert>
<div class="d-flex align-center mb-3 text-body-2 text-medium-emphasis" data-testid="np-boost-hint">
<span>Как увеличить количество сделок</span>
<v-tooltip
text="Ваш лимит распределяется на нескольких поставщиков равномерно. Даже если лимит не выбирается полностью, просто увеличьте лимит — и сделок придёт больше."
location="top"
max-width="280"
>
<template #activator="{ props: tip }">
<v-icon
v-bind="tip"
size="14"
class="src-hint ml-1"
icon="mdi-help-circle-outline"
aria-label="Как увеличить количество сделок"
tabindex="0"
/>
</template>
</v-tooltip>
</div>
<div class="d-flex align-center mb-1 text-body-2 text-medium-emphasis">
<span>Откуда собирать заявки</span>
<v-tooltip
text="Сайт — заявки с сайтов вашей ниши. Звонок — звонки на номер. СМС — заявки по СМС. Выберите подходящий источник."
location="top"
max-width="280"
>
<template #activator="{ props: tip }">
<v-icon
v-bind="tip"
size="14"
class="src-hint ml-1"
icon="mdi-help-circle-outline"
aria-label="Чем отличаются источники"
tabindex="0"
/>
</template>
</v-tooltip>
</div>
<v-tabs v-model="form.signal_type" :disabled="mode === 'edit'" color="primary">
<v-tab value="site"><v-icon start>mdi-web</v-icon>Сайт</v-tab>
<v-tab value="call"><v-icon start>mdi-phone</v-icon>Звонок</v-tab>
<v-tab value="sms"><v-icon start>mdi-message-text</v-icon>СМС</v-tab>
</v-tabs>
<v-tabs-window v-model="form.signal_type" class="mt-4">
<v-tabs-window-item value="site">
<div class="source-hint text-caption text-medium-emphasis mb-2">
Укажите сайт в вашей нише, по теме которого нужны заявки приведём вам таких же клиентов
</div>
<v-text-field
v-model="form.signal_identifier"
label="Сайт, с которого нужны заявки"
placeholder="okna-moskva.ru"
class="ld-input-quiet"
data-testid="np-source-identifier"
:error-messages="errors.signal_identifier"
/>
</v-tabs-window-item>
<v-tabs-window-item value="call">
<div class="source-hint text-caption text-medium-emphasis mb-2">
Укажите телефон в вашей нише, по звонкам на который соберём вам заявки
</div>
<v-text-field
v-model="form.signal_identifier"
label="Телефон, по которому идут клиенты"
placeholder="79161234567"
hint="Можно вводить с +7, 8, скобками и пробелами — приведём к виду 79161234567"
persistent-hint
class="ld-input-quiet"
data-testid="np-source-identifier"
:error-messages="errors.signal_identifier"
/>
</v-tabs-window-item>
<v-tabs-window-item value="sms">
<div class="source-hint text-caption text-medium-emphasis mb-2">
Источник отправитель SMS и (опционально) ключевое слово в тексте
</div>
<v-combobox
v-model="form.sms_senders"
label="Отправители (до 11 символов каждый)"
multiple
chips
clearable
:error-messages="errors.sms_senders"
/>
<v-text-field
v-model="form.sms_keyword"
label="Ключевое слово (опционально)"
hint="Если пусто — проект подключится только к B3"
class="ld-input-quiet"
:error-messages="errors.sms_keyword"
/>
</v-tabs-window-item>
</v-tabs-window>
<!-- Эпик 3.1: объявление о вступлении правок количества/региона/дней (без блокировки). -->
<v-alert
v-if="showAppliesFromBanner"
type="info"
variant="tonal"
density="comfortable"
class="mt-3"
data-testid="np-applies-from-banner"
>
Ближайший сбор уже идёт по текущим настройкам. Новые количество, регионы
и дни приёма вступят в силу с {{ appliesFromDate }}.
</v-alert>
<v-divider class="my-4" />
<v-text-field
v-model="form.name"
label="Название проекта"
class="ld-input-quiet"
:error-messages="errors.name"
/>
<v-text-field
v-model.number="form.daily_limit_target"
label="Лимит лидов в день"
type="number"
min="1"
max="10000"
class="ld-input-quiet"
data-testid="np-limit"
hint="Сколько заявок в день вы готовы принимать и оплачивать. Можно менять в любой момент."
persistent-hint
:error-messages="errors.daily_limit_target"
/>
<v-autocomplete
v-model:search="regionSearch"
:model-value="form.regions"
:items="selectableRegions"
item-title="name"
item-value="code"
label="Регионы"
:disabled="vsyaRfConfirmed"
multiple
chips
closable-chips
clearable
density="comfortable"
class="ld-input-quiet"
data-testid="regions-autocomplete"
:error-messages="errors.regions"
@update:model-value="onRegionsChange"
>
<template #item="{ props: itemProps, item }">
<v-list-item v-bind="itemProps">
<template #subtitle>
{{ FEDERAL_DISTRICT_NAMES[item.raw.federalDistrict] || '' }}
</template>
</v-list-item>
</template>
</v-autocomplete>
<v-checkbox
:model-value="vsyaRf"
label="Вся РФ (все регионы)"
density="comfortable"
hide-details
data-testid="vsya-rf-checkbox"
@update:model-value="(v: boolean | null) => (v ? chooseVsyaRf() : cancelVsyaRf())"
/>
<v-alert
v-if="vsyaRf"
type="info"
variant="tonal"
density="compact"
class="mt-2"
data-testid="vsya-rf-confirmed"
>
Проект будет получать лиды по всей России — по всем субъектам страны.
</v-alert>
<v-alert
v-if="generalError"
type="error"
variant="tonal"
density="compact"
class="mt-3"
closable
@click:close="generalError = null"
>
{{ generalError }}
</v-alert>
<div class="mt-3">
<span class="text-caption">Дни недели приёма</span>
<v-btn-toggle v-model="selectedDays" multiple density="comfortable" class="mt-1">
<v-btn v-for="(day, i) in dayLabels" :key="i" :value="i">{{ day }}</v-btn>
</v-btn-toggle>
<div class="mt-1">
<v-btn size="small" variant="text" @click="setWorkdays('weekdays')">Будни</v-btn>
<v-btn size="small" variant="text" @click="setWorkdays('all')">Все дни</v-btn>
</div>
</div>
</template>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn variant="text" @click="close">Отмена</v-btn>
<v-btn
v-if="step === 'requisites'"
color="primary"
:loading="reqSaving"
data-testid="req-next-btn"
@click="saveRequisites"
>
Далее: к проекту
</v-btn>
<v-btn v-else color="primary" :loading="saving" data-testid="submit-btn" @click="submit">
{{ mode === 'edit' ? 'Сохранить' : 'Создать' }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<ProjectLimitOverloadDialog
v-model="overloadOpen"
:payload="overloadPayload"
@save-blocked="onOverloadSaveBlocked"
@set-zero="onOverloadSetZero"
/>
<!-- Эпик 3.2: подтверждение смены источника на залоченном проекте. -->
<v-dialog v-model="sourceConfirmOpen" max-width="430">
<v-card data-testid="np-source-change-confirm">
<v-card-title>Сменить источник?</v-card-title>
<v-card-text>{{ sourceConfirmText }}</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn variant="text" data-testid="np-source-confirm-no" @click="cancelSourceChange">Отмена</v-btn>
<v-btn color="primary" data-testid="np-source-confirm-yes" @click="confirmSourceChange">
Сменить источник
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, watch, computed } from 'vue';
import { apiClient, ensureCsrfCookie, extractErrorMessage } from '../../api/client';
import { getRequisites, updateRequisites, type Requisites } from '../../api/requisites';
import { firstLeadDate, formatLeadDate } from '../../utils/leadDate';
import { REGIONS, FEDERAL_DISTRICT_NAMES } from '../../constants/regions';
import type { Project } from '../../stores/projectsStore';
import DevIndexBadge from '../../components/DevIndexBadge.vue';
import ProjectLimitOverloadDialog from '../../components/projects/ProjectLimitOverloadDialog.vue';
const selectableRegions = REGIONS.filter((r) => r.code !== 0);
// Дата старта лидов для баннера нового проекта (правило слепка 18:00 МСК).
const leadStart = computed(() => firstLeadDate());
// Эпик 3 (редизайн 2026-06-25): в edit-режиме источник редактируем (не readonly).
// Замок заменён объявлением о вступлении + подтверждением смены источника —
// раздача доводит хвост по старому источнику через слепок.
const sourceLocked = computed(() => props.mode === 'edit' && props.project?.source_locked === true);
const slepokFieldsDirty = computed(() => {
const p = props.project;
if (!p) return false;
const regionsChanged = JSON.stringify([...form.regions].sort()) !== JSON.stringify([...(p.regions ?? [])].sort());
return (
form.daily_limit_target !== p.daily_limit_target ||
form.delivery_days_mask !== (p.delivery_days_mask ?? 127) ||
regionsChanged
);
});
const showAppliesFromBanner = computed(() => sourceLocked.value && slepokFieldsDirty.value);
// Time-aware дата вступления (правило слепка 18:00 МСК) — объявление актуально по времени суток.
const appliesFromDate = computed(() => firstLeadDate());
const sourceDirty = computed(() => {
const p = props.project;
if (!p) return false;
if (form.signal_type === 'sms') {
return (
JSON.stringify(form.sms_senders) !== JSON.stringify(p.sms_senders ?? []) ||
form.sms_keyword !== (p.sms_keyword ?? '')
);
}
return form.signal_identifier !== (p.signal_identifier ?? '');
});
const sourceConfirmOpen = ref(false);
const sourceConfirmText = computed(() => {
// Эпик 6.3: единый текст правила из API (ProjectRuleMessages). Fallback — локальный.
const fromApi = props.project?.source_change_message;
if (fromApi) {
return `Мы уже ведём сбор на завтра. ${fromApi} Подтвердите смену источника.`;
}
const d = formatLeadDate(props.project?.source_unlock_at);
return (
'Мы уже ведём сбор на завтра. Лиды по старому источнику придут завтра в любом случае. ' +
(d ? `Послезавтра (после ${d}) — уже по новому. ` : '') +
'Подтвердите смену источника.'
);
});
function cancelSourceChange(): void {
sourceConfirmOpen.value = false;
}
async function confirmSourceChange(): Promise<void> {
sourceConfirmOpen.value = false;
await persist();
}
const props = defineProps<{
modelValue: boolean;
mode?: 'create' | 'edit';
project?: Project | null;
}>();
const emit = defineEmits<{
'update:modelValue': [value: boolean];
saved: [appliesFrom: string | null];
}>();
// Plan 6: regions = subject codes (1..89) — backend dual-writes region_mask/region_mode.
// Пустой массив = вся РФ.
const form = reactive({
name: '',
signal_type: 'site' as 'site' | 'call' | 'sms',
signal_identifier: '',
sms_senders: [] as string[],
sms_keyword: '',
daily_limit_target: 50,
regions: [] as number[],
delivery_days_mask: 127,
});
const errors = reactive<Record<string, string[]>>({});
const saving = ref(false);
const generalError = ref<string | null>(null);
// Косяк 04 / вариант C: визард создания. step='requisites' — новый клиент без
// реквизитов заполняет их прямо здесь (шаг 1), затем step='project' (шаг 2).
// Реквизиты неполны ⟺ проектов ещё нет (гейт G1/SP2 не пускал без них), поэтому
// решение по шагу принимаем по light-complete, без отдельного счётчика проектов.
const step = ref<'requisites' | 'project'>('project');
const reqForm = reactive({ subject_type: '', contact_name: '', contact_phone: '', inn: '' });
const reqErrors = reactive<Record<string, string[]>>({});
const reqSaving = ref(false);
const reqGeneralError = ref<string | null>(null);
const subjectTypeItems = [
{ value: 'individual', title: 'Физлицо' },
{ value: 'sole_proprietor', title: 'ИП' },
{ value: 'legal_entity', title: 'Юрлицо' },
];
// Зеркало RequisitesService::isLightComplete — тип лица + имя + телефон (+ ИНН для юр/ИП).
function reqLightComplete(r: Requisites | null): boolean {
if (!r || !r.subject_type || !r.contact_name?.trim() || !r.contact_phone?.trim()) {
return false;
}
if ((r.subject_type === 'legal_entity' || r.subject_type === 'sole_proprietor') && !r.inn?.trim()) {
return false;
}
return true;
}
async function initCreateStep(): Promise<void> {
step.value = 'project';
try {
const r = await getRequisites();
if (!reqLightComplete(r)) {
step.value = 'requisites';
if (r) {
reqForm.subject_type = r.subject_type ?? '';
reqForm.contact_name = r.contact_name ?? '';
reqForm.contact_phone = r.contact_phone ?? '';
reqForm.inn = r.inn ?? '';
}
}
} catch {
// fail-open: при ошибке чтения не блокируем — бэкенд-гейт защитит на submit (422 → шаг 1).
step.value = 'project';
}
}
async function saveRequisites(): Promise<void> {
Object.keys(reqErrors).forEach((k) => delete reqErrors[k]);
reqGeneralError.value = null;
reqSaving.value = true;
try {
await updateRequisites({
subject_type: reqForm.subject_type as Requisites['subject_type'],
contact_name: reqForm.contact_name,
contact_phone: reqForm.contact_phone,
inn: reqForm.inn || null,
});
step.value = 'project';
} catch (e: unknown) {
const err = e as { response?: { status?: number; data?: { errors?: Record<string, string[]> } } };
if (err.response?.status === 422 && err.response.data?.errors) {
Object.assign(reqErrors, err.response.data.errors);
} else {
reqGeneralError.value = extractErrorMessage(e);
}
} finally {
reqSaving.value = false;
}
}
// Spec C §6.2: префлайт баланса — диалог перегрузки лимита по 409.
interface OverloadPayloadShape {
current_balance_rub: string;
current_capacity_leads: number;
would_be_required_leads: number;
deficit_leads: number;
}
const overloadOpen = ref(false);
const overloadPayload = ref<OverloadPayloadShape | null>(null);
// Plan 4 Task 4 + косяк 03: явная «Вся РФ» одной галочкой (защита от случайной
// «всей РФ» сохранена — нужен осознанный клик), без отдельной кнопки подтверждения.
// vsyaRf — чекбокс выбран; vsyaRfConfirmed — true сразу при установке галочки.
// На бэке regions=[] (Вся РФ) и «забыл» неотличимы → гейт намеренно UI-only.
const vsyaRf = ref(false);
const vsyaRfConfirmed = ref(false);
function chooseVsyaRf(): void {
vsyaRf.value = true;
confirmVsyaRf();
}
function confirmVsyaRf(): void {
vsyaRfConfirmed.value = true;
form.regions = []; // Вся РФ → пустой массив субъектов
delete errors.regions; // косяк 03: галочка сразу снимает зависшую ошибку
}
function cancelVsyaRf(): void {
vsyaRf.value = false;
vsyaRfConfirmed.value = false;
}
function onRegionsChange(codes: number[]): void {
form.regions = Array.isArray(codes) ? codes : [];
// После выбора субъекта очищаем строку поиска (иначе набранное «красно»
// остаётся в поле рядом с чипом).
regionSearch.value = '';
if (form.regions.length > 0) {
// Взаимоисключение: выбор конкретных субъектов снимает «Вся РФ».
vsyaRf.value = false;
vsyaRfConfirmed.value = false;
delete errors.regions;
}
}
const regionSearch = ref('');
const dayLabels = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс'];
const selectedDays = ref<number[]>([0, 1, 2, 3, 4, 5, 6]);
watch(selectedDays, (days) => {
form.delivery_days_mask = days.reduce((acc, d) => acc | (1 << d), 0);
});
function setWorkdays(preset: 'weekdays' | 'all') {
if (preset === 'weekdays') selectedDays.value = [0, 1, 2, 3, 4];
else selectedDays.value = [0, 1, 2, 3, 4, 5, 6];
}
watch(
() => props.modelValue,
(open) => {
if (open) generalError.value = null;
if (open) {
delete errors.regions;
}
if (open && props.mode === 'edit' && props.project) {
step.value = 'project'; // редактирование — всегда форма проекта, без шага реквизитов
Object.assign(form, props.project);
form.regions = Array.isArray(props.project.regions) ? [...props.project.regions] : [];
const days: number[] = [];
for (let i = 0; i < 7; i++) if (form.delivery_days_mask & (1 << i)) days.push(i);
selectedDays.value = days;
// Существующий проект с пустыми регионами = «Вся РФ» (предзаполняем подтверждённым).
vsyaRf.value = form.regions.length === 0;
vsyaRfConfirmed.value = form.regions.length === 0;
} else if (open) {
Object.assign(form, {
name: '',
signal_type: 'site',
signal_identifier: '',
sms_senders: [],
sms_keyword: '',
daily_limit_target: 50,
regions: [],
delivery_days_mask: 127,
});
selectedDays.value = [0, 1, 2, 3, 4, 5, 6];
vsyaRf.value = false;
vsyaRfConfirmed.value = false;
// Косяк 04 / вариант C: решаем, нужен ли шаг реквизитов (новый клиент без них).
Object.assign(reqForm, { subject_type: '', contact_name: '', contact_phone: '', inn: '' });
Object.keys(reqErrors).forEach((k) => delete reqErrors[k]);
reqGeneralError.value = null;
void initCreateStep();
}
},
{ immediate: true },
);
async function persist(extra: Record<string, unknown> = {}): Promise<void> {
saving.value = true;
try {
await ensureCsrfCookie();
const body: Record<string, unknown> = { ...form, ...extra };
// M (балансовый блок findings): для site/call не отправляем sms-поля. Форма
// держит sms_senders=null/[] и sms_keyword, а UpdateProjectRequest валидирует
// sms_senders как array|min:1 → null давал молчаливый 422 (поле sms на форме
// site/call не отрисовано, ошибка не видна). Заодно не триггерим зря snapshot-guard.
if (form.signal_type !== 'sms') {
delete body.sms_senders;
delete body.sms_keyword;
}
let appliesFrom: string | null = null;
if (props.mode === 'edit' && props.project) {
const { data } = await apiClient.patch(`/api/projects/${props.project.id}`, body);
// Backend кладёт applies_from только когда правка задела slepok-чувствительные поля.
appliesFrom = data?.data?.applies_from ?? null;
} else {
await apiClient.post('/api/projects', body);
// Create НЕ генерирует applies_from (новый проект сразу попадает в snapshot).
}
overloadOpen.value = false;
emit('saved', appliesFrom);
close();
} catch (e: unknown) {
const err = e as {
response?: { status?: number; data?: { error?: string; errors?: Record<string, string[]> } };
};
// G1/SP3b + косяк 04: гейт первого проекта — показываем шаг реквизитов
// прямо в этом окне (запасной путь, обычно шаг 1 показан ещё при открытии).
if (err.response?.status === 422 && err.response.data?.error === 'requisites_required') {
step.value = 'requisites';
void initCreateStep();
}
// Spec C §6.2: лимит превышает баланс — открываем диалог перегрузки.
else if (err.response?.status === 409 && err.response.data?.error === 'balance_insufficient') {
overloadPayload.value = err.response.data as OverloadPayloadShape;
overloadOpen.value = true;
} else if (err.response?.status === 422 && err.response.data?.errors) {
Object.assign(errors, err.response.data.errors);
} else {
generalError.value = extractErrorMessage(e);
}
} finally {
saving.value = false;
}
}
async function submit() {
generalError.value = null;
Object.keys(errors).forEach((k) => delete errors[k]);
// F-NEWPROJECT-1: клиентская валидация обязательных полей — показываем ошибку
// сразу, не дожидаясь ответа сервера (сервер всё равно валидирует повторно).
const clientErrors: Record<string, string[]> = {};
if (!form.name.trim()) {
clientErrors.name = ['Введите название проекта'];
}
if (form.signal_type === 'sms') {
if (form.sms_senders.length === 0) {
clientErrors.sms_senders = ['Укажите хотя бы одного отправителя'];
}
} else if (!form.signal_identifier.trim()) {
clientErrors.signal_identifier = [
form.signal_type === 'call' ? 'Введите номер в вашей нише' : 'Введите домен сайта в вашей нише',
];
}
// Гейт обязательного региона: нужны либо субъекты, либо подтверждённая «Вся РФ».
if (form.regions.length === 0 && !vsyaRfConfirmed.value) {
clientErrors.regions = ['Выберите регион или подтвердите «Вся РФ»'];
}
if (Object.keys(clientErrors).length > 0) {
Object.assign(errors, clientErrors);
return;
}
// Эпик 3.2: смена источника на залоченном проекте — через подтверждение.
if (sourceLocked.value && sourceDirty.value) {
sourceConfirmOpen.value = true;
return;
}
await persist();
}
// Spec C §6.2 — исходы диалога перегрузки лимита.
async function onOverloadSaveBlocked(): Promise<void> {
await persist({ force_save_blocked: true });
}
async function onOverloadSetZero(): Promise<void> {
form.daily_limit_target = 0;
overloadOpen.value = false;
await persist();
}
function close() {
emit('update:modelValue', false);
}
defineExpose({
chooseVsyaRf,
confirmVsyaRf,
cancelVsyaRf,
onRegionsChange,
vsyaRf,
vsyaRfConfirmed,
form,
submit,
step,
reqForm,
saveRequisites,
});
</script>
<style scoped>
.source-hint {
line-height: 1.4;
padding: 4px 2px;
}
.steps-head {
display: flex;
align-items: center;
gap: 10px;
font-size: 13px;
}
.steps-head .step-on {
color: var(--liderra-teal, #0f6e56);
font-weight: 650;
}
.steps-head .step-off {
color: #6b6f72;
}
.steps-head .step-sep {
flex: 1;
height: 1px;
background: var(--liderra-line, #e6e2d6);
}
.ld-input-quiet :deep(.v-field) {
border-radius: var(--radius-8);
}
.ld-input-quiet :deep(.v-field__outline__start),
.ld-input-quiet :deep(.v-field__outline__end),
.ld-input-quiet :deep(.v-field__outline__notch::before),
.ld-input-quiet :deep(.v-field__outline__notch::after) {
border-color: var(--liderra-line);
opacity: 1;
transition: border-color 200ms cubic-bezier(0.16, 1, 0.3, 1);
}
.ld-input-quiet :deep(.v-field:hover .v-field__outline__start),
.ld-input-quiet :deep(.v-field:hover .v-field__outline__end),
.ld-input-quiet :deep(.v-field:hover .v-field__outline__notch::before),
.ld-input-quiet :deep(.v-field:hover .v-field__outline__notch::after) {
border-color: var(--liderra-line-strong);
opacity: 1;
}
.ld-input-quiet :deep(.v-field--focused .v-field__outline__start),
.ld-input-quiet :deep(.v-field--focused .v-field__outline__end),
.ld-input-quiet :deep(.v-field--focused .v-field__outline__notch::before),
.ld-input-quiet :deep(.v-field--focused .v-field__outline__notch::after) {
border-color: var(--liderra-teal);
opacity: 1;
}
.ld-input-quiet :deep(.v-field--error:not(.v-field--disabled) .v-field__outline__start),
.ld-input-quiet :deep(.v-field--error:not(.v-field--disabled) .v-field__outline__end),
.ld-input-quiet :deep(.v-field--error:not(.v-field--disabled) .v-field__outline__notch::before),
.ld-input-quiet :deep(.v-field--error:not(.v-field--disabled) .v-field__outline__notch::after) {
border-color: currentColor;
opacity: 1;
}
</style>