Add viewport controls, collapsible panels with persisted state, filters modal, and refined context menus; preserve map layout on navigation and let contacts be edited with relations on /contacts/:id instead of list modals. Co-authored-by: Cursor <cursoragent@cursor.com>
205 lines
6.2 KiB
Vue
205 lines
6.2 KiB
Vue
<template>
|
|
<form @submit.prevent="onSubmit">
|
|
<div class="form-group">
|
|
<label>Имя *</label>
|
|
<input v-model="form.name" class="form-control" placeholder="Фамилия Имя Отчество" required />
|
|
</div>
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
|
<div class="form-group">
|
|
<label>Email</label>
|
|
<input v-model="form.email" type="email" class="form-control" placeholder="email@example.com" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Телефон</label>
|
|
<input v-model="form.phone" class="form-control" placeholder="+7-900-000-0000" />
|
|
</div>
|
|
</div>
|
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;">
|
|
<div class="form-group">
|
|
<label>Организация</label>
|
|
<input v-model="form.organization" class="form-control" placeholder="ООО Компания" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Должность</label>
|
|
<input v-model="form.position" class="form-control" placeholder="Директор" />
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Карты сети</label>
|
|
<div v-if="!availableMaps.length" class="text-muted" style="font-size:13px;">
|
|
Нет карт. Создайте карту на странице «Карта сети».
|
|
</div>
|
|
<div v-else class="map-checkboxes">
|
|
<label v-for="map in availableMaps" :key="map.id" class="checkbox-label">
|
|
<input
|
|
v-model="form.mapIds"
|
|
type="checkbox"
|
|
:value="String(map.id)"
|
|
/>
|
|
{{ map.name }}
|
|
</label>
|
|
</div>
|
|
<p class="checkbox-hint">На странице «Граф» контакт виден всегда; на выбранных картах — в соответствующих визуализациях.</p>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Заметки</label>
|
|
<textarea v-model="form.notes" class="form-control" placeholder="Дополнительная информация..." rows="3"></textarea>
|
|
</div>
|
|
<RelationLinkFields
|
|
v-if="showRelationLink"
|
|
ref="relationLinkRef"
|
|
:options="linkToOptions"
|
|
:initial-target-id="initialLinkToId"
|
|
:conflict-mode="conflictMode"
|
|
/>
|
|
<component
|
|
:is="Ext"
|
|
v-for="(Ext, index) in contactFormExtensions"
|
|
:key="index"
|
|
:contact-id="initial?.id"
|
|
v-model="pluginTags"
|
|
/>
|
|
<div class="contact-form-footer">
|
|
<button
|
|
v-if="showDelete"
|
|
type="button"
|
|
class="btn btn-danger"
|
|
@click="$emit('delete')"
|
|
>
|
|
Удалить
|
|
</button>
|
|
<div class="contact-form-actions">
|
|
<button type="button" class="btn btn-secondary" @click="$emit('cancel')">Отмена</button>
|
|
<button type="submit" class="btn btn-primary">Сохранить</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { reactive, ref, watch, computed, onMounted } from 'vue'
|
|
import { useNetworkMapsStore } from '../stores/networkMaps'
|
|
import { listMembershipsByContact } from '../application/usecases/networkMaps'
|
|
import { getContactFormExtensions } from '../core/pluginRegistry'
|
|
import RelationLinkFields from './RelationLinkFields.vue'
|
|
|
|
const props = defineProps({
|
|
initial: { type: Object, default: () => ({}) },
|
|
initialMapIds: { type: Array, default: null },
|
|
deletable: { type: Boolean, default: false },
|
|
showRelationLink: { type: Boolean, default: false },
|
|
linkToOptions: { type: Array, default: () => [] },
|
|
initialLinkToId: { type: [String, Number], default: '' },
|
|
conflictMode: { type: Boolean, default: false },
|
|
})
|
|
const emit = defineEmits(['submit', 'cancel', 'delete'])
|
|
|
|
const mapsStore = useNetworkMapsStore()
|
|
const contactFormExtensions = getContactFormExtensions()
|
|
const pluginTags = ref([])
|
|
const relationLinkRef = ref(null)
|
|
|
|
const showDelete = computed(() => {
|
|
if (props.deletable) return true
|
|
const id = props.initial?.id
|
|
return id !== undefined && id !== null && id !== ''
|
|
})
|
|
|
|
const availableMaps = computed(() => mapsStore.maps)
|
|
|
|
const form = reactive({
|
|
name: props.initial.name || '',
|
|
email: props.initial.email || '',
|
|
phone: props.initial.phone || '',
|
|
organization: props.initial.organization || '',
|
|
position: props.initial.position || '',
|
|
notes: props.initial.notes || '',
|
|
mapIds: (props.initialMapIds || []).map(String),
|
|
})
|
|
|
|
async function loadMapIdsForContact(contactId) {
|
|
if (!contactId) {
|
|
form.mapIds = []
|
|
return
|
|
}
|
|
const memberships = await listMembershipsByContact(contactId)
|
|
form.mapIds = memberships.map((m) => String(m.mapId || m.map))
|
|
}
|
|
|
|
watch(() => props.initial, async (v) => {
|
|
if (!v || !Object.keys(v).length) {
|
|
Object.assign(form, {
|
|
name: '',
|
|
email: '',
|
|
phone: '',
|
|
organization: '',
|
|
position: '',
|
|
notes: '',
|
|
mapIds: [],
|
|
})
|
|
return
|
|
}
|
|
Object.assign(form, {
|
|
name: v.name || '',
|
|
email: v.email || '',
|
|
phone: v.phone || '',
|
|
organization: v.organization || '',
|
|
position: v.position || '',
|
|
notes: v.notes || '',
|
|
})
|
|
if (props.initialMapIds) {
|
|
form.mapIds = props.initialMapIds.map(String)
|
|
} else if (v.id) {
|
|
await loadMapIdsForContact(v.id)
|
|
}
|
|
}, { deep: true })
|
|
|
|
onMounted(async () => {
|
|
await mapsStore.fetchMaps()
|
|
if (props.initial?.id && !props.initialMapIds) {
|
|
await loadMapIdsForContact(props.initial.id)
|
|
}
|
|
})
|
|
|
|
function onSubmit() {
|
|
const { mapIds, ...contactData } = form
|
|
const relationLink = props.showRelationLink
|
|
? relationLinkRef.value?.getRelationLink?.() ?? null
|
|
: null
|
|
emit('submit', contactData, mapIds, { tags: [...pluginTags.value] }, relationLink)
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.map-checkboxes {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
.checkbox-label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
}
|
|
.checkbox-hint {
|
|
margin: 6px 0 0;
|
|
font-size: 12px;
|
|
color: var(--text-muted);
|
|
line-height: 1.4;
|
|
}
|
|
.contact-form-footer {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
margin-top: 22px;
|
|
}
|
|
.contact-form-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
margin-left: auto;
|
|
}
|
|
</style>
|