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>
@
103 lines
3.8 KiB
Vue
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>
|