Files
portal/app/resources/js/components/sales/ProspectContactsEditor.vue
T
Дмитрий 68428f16c6 @
feat(sales): карточка кандидата — девять полей владельца + правка контактных лиц

Владелец задал список полей карточки и попросил дать менеджеру записывать
контактных лиц прямо в карточке: поиск отдаёт 5-7 ПРЕДПОЛАГАЕМЫХ номеров
директора, живой из них выясняется обзвоном; а если директор перенаправил
к маркетологу — менеджер фиксирует и его, людей может быть 2-3.

- Карточка показывает РОВНО девять полей в его порядке: Юрлицо, Сайт, ИНН фирмы,
  Адрес, Телефон общий, Каналы, Бюджет, Директор, Тел. директора (предполагаемый).
  Убраны Оценка, Достоверность, Реклама, Запросы в Директе, ОГРН, Статус юрлица,
  Личный ИНН директора, Почта директора — данные остаются в payload.
- PATCH /api/sales/prospects/{id}/contacts — список заменяется целиком, пустой
  стирает всех; права как у update() (менеджер свои, начальник любые); главный
  телефон карточки из поиска не трогается.
- ProspectContactsEditor.vue + utils/prospectContacts.ts — один редактор контактов
  на диалог создания и карточку; диалог создания переведён на него.
- Закрывает хвост §16: у 24 живых карточек из поиска контактных лиц не было,
  теперь дозаполняются руками.

Спека §18. Гейты: Pest 223/223 sales, Vitest 196 файлов / 1397 тестов,
vue-tsc чисто, Larastan 0 в своих файлах. TDD RED→GREEN на каждом шаге.

LEFTHOOK_EXCLUDE: larastan/cspell падают на файлах параллельной сессии
tests/Feature/Admin/*Balances*, свои файлы проверены отдельно и чисты.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@
2026-07-18 12:29:04 +03:00

103 lines
3.8 KiB
Vue

<script setup lang="ts">
/**
* Редактор контактных лиц кандидата — один на два места: диалог «Добавить кандидата»
* и карточка в воронке.
*
* Зачем в карточке: из поиска приходит 5–7 ПРЕДПОЛАГАЕМЫХ номеров директора. Какой
* из них живой, менеджер узнаёт только обзвонив. А если директор сказал «этим
* занимается маркетолог, вот его телефон» — записывает и его, людей может быть 2–3.
*
* Спека §16 / §18.
*/
import { ref, watch } from 'vue';
import { emptyContact, type ContactForm } from '../../utils/prospectContacts';
const props = defineProps<{ modelValue: ContactForm[] }>();
const emit = defineEmits<{ 'update:modelValue': [v: ContactForm[]] }>();
const rows = ref<ContactForm[]>(props.modelValue.length > 0 ? props.modelValue : [emptyContact()]);
// Родитель подменил список (открыли другую карточку) — перезаполняем.
watch(
() => props.modelValue,
(v) => {
rows.value = v.length > 0 ? v : [emptyContact()];
},
);
function push(): void {
emit('update:modelValue', rows.value);
}
function addContact(): void {
rows.value.push(emptyContact());
push();
}
/** Последнего человека не удаляем совсем — оставляем пустую строку, чтобы было куда писать. */
function removeContact(i: number): void {
rows.value.splice(i, 1);
if (rows.value.length === 0) rows.value.push(emptyContact());
push();
}
function addPhone(i: number): void {
rows.value[i].phones.push('');
push();
}
function removePhone(i: number, j: number): void {
rows.value[i].phones.splice(j, 1);
if (rows.value[i].phones.length === 0) rows.value[i].phones.push('');
push();
}
defineExpose({ rows, addContact, removeContact, addPhone, removePhone });
</script>
<template>
<div>
<v-sheet v-for="(c, i) in rows" :key="i" class="pa-3 mb-3" border rounded>
<div class="d-flex align-start ga-2">
<v-text-field
v-model="c.name"
label="ФИО"
density="compact"
variant="outlined"
hide-details
@update:model-value="push"
/>
<v-text-field
v-model="c.position"
label="Должность"
density="compact"
variant="outlined"
hide-details
@update:model-value="push"
/>
<v-btn icon="mdi-close" size="small" variant="text" title="убрать человека" @click="removeContact(i)" />
</div>
<div v-for="(_, j) in c.phones" :key="j" class="d-flex align-center ga-2 mt-2">
<v-text-field
v-model="c.phones[j]"
label="Телефон"
density="compact"
variant="outlined"
hide-details
@update:model-value="push"
/>
<v-btn icon="mdi-close" size="small" variant="text" title="убрать телефон" @click="removePhone(i, j)" />
</div>
<v-btn class="mt-2" size="small" variant="text" prepend-icon="mdi-plus" @click="addPhone(i)">
Ещё телефон
</v-btn>
</v-sheet>
<v-btn size="small" variant="tonal" prepend-icon="mdi-account-plus" @click="addContact">
Ещё человек
</v-btn>
</div>
</template>