Add network map UX and theme support.

Introduce network map view/components with filtering and positioning, expand contacts/map backend fields and migrations, and add a persistent light/dark theme toggle with graph label color updates for readability.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-28 13:24:48 +03:00
co-authored by Cursor
parent 81ba6cd076
commit b668ee8507
49 changed files with 4679 additions and 129 deletions
+62 -1
View File
@@ -6,7 +6,14 @@ from rest_framework import viewsets, status
from rest_framework.decorators import api_view, action
from rest_framework.response import Response
from .models import Contact, Relation, RELATION_TYPES
from .models import (
Contact,
Relation,
RELATION_TYPES,
LIFE_SPHERES,
NETWORK_CIRCLES,
INTERACTION_INTENSITY,
)
from .serializers import ContactSerializer, RelationSerializer, GraphSerializer
@@ -37,6 +44,11 @@ def graph_data(request):
'label': c.name,
'title': '\n'.join(filter(None, [c.organization, c.position, c.email])),
'group': c.organization or 'default',
'life_sphere': c.life_sphere,
'network_circle': c.network_circle,
'importance': c.importance,
'map_angle': c.map_angle,
'map_radius_ratio': c.map_radius_ratio,
}
for c in contacts
]
@@ -49,18 +61,67 @@ def graph_data(request):
'label': r.get_relation_type_display(),
'title': r.description or r.get_relation_type_display(),
'relation_type': r.relation_type,
'interaction_intensity': r.interaction_intensity,
}
for r in relations
]
return Response({'nodes': nodes, 'edges': edges})
@api_view(['GET'])
def network_map_graph(request):
"""Граф только для карты сети: контакты с include_on_network_map и связи между ними."""
contacts = list(
Contact.objects.filter(include_on_network_map=True).order_by('name')
)
allowed_ids = {c.id for c in contacts}
nodes = [
{
'id': c.id,
'label': c.name,
'title': '\n'.join(filter(None, [c.organization, c.position, c.email])),
'group': c.organization or 'default',
'life_sphere': c.life_sphere,
'network_circle': c.network_circle,
'importance': c.importance,
'map_angle': c.map_angle,
'map_radius_ratio': c.map_radius_ratio,
}
for c in contacts
]
relations = Relation.objects.select_related('source', 'target').all()
edges = [
{
'id': r.id,
'from': r.source_id,
'to': r.target_id,
'label': r.get_relation_type_display(),
'title': r.description or r.get_relation_type_display(),
'relation_type': r.relation_type,
'interaction_intensity': r.interaction_intensity,
}
for r in relations
if r.source_id in allowed_ids and r.target_id in allowed_ids
]
return Response({'nodes': nodes, 'edges': edges})
@api_view(['GET'])
def relation_types(request):
"""Список допустимых типов связей."""
return Response([{'value': v, 'label': l} for v, l in RELATION_TYPES])
@api_view(['GET'])
def network_map_choices(request):
"""Подписи для карты сети: сферы, круги, интенсивность связей."""
return Response({
'life_spheres': [{'value': v, 'label': l} for v, l in LIFE_SPHERES],
'network_circles': [{'value': v, 'label': l} for v, l in NETWORK_CIRCLES],
'interaction_intensities': [{'value': v, 'label': l} for v, l in INTERACTION_INTENSITY],
})
def _monica_contact_fields(contact_data):
"""Из вложенного data контакта Monica (экспорт account.data) извлекает телефон, email, заметки."""
phone = ''