mirror of
https://github.com/vcscsvcscs/GenerationsHeritage.git
synced 2025-08-12 13:59:08 +02:00
Compare commits
20 Commits
feature/mi
...
305004d06a
Author | SHA1 | Date | |
---|---|---|---|
305004d06a | |||
cb86748281 | |||
cb47d30627 | |||
3730e52361 | |||
0ce1b35b3f | |||
5538435b6e | |||
8d37fc0fe3 | |||
f801e8893a | |||
82dc8d8e08 | |||
57ac9c068a | |||
a77f0f434e | |||
3ed3c037ab | |||
46a612e31d | |||
8358a38f4d | |||
d393959c0d | |||
4f67a973a2 | |||
40e557f8c7 | |||
7397ba0ccc | |||
58bb20e608 | |||
|
c706785e51 |
@@ -4,6 +4,7 @@
|
||||
"accept": "Accept",
|
||||
"add": "Add",
|
||||
"add_administrator": "Add administrator",
|
||||
"add_note": "Add Note",
|
||||
"add_relationship": "Add Relationship",
|
||||
"address": "Address",
|
||||
"admin": "Admin",
|
||||
@@ -160,6 +161,7 @@
|
||||
"until": "Until",
|
||||
"vaccination": "Vaccination",
|
||||
"vegetable": "Vegetable",
|
||||
"verified": "Verified",
|
||||
"video": "Video",
|
||||
"website": "Website",
|
||||
"weight": "Weight",
|
||||
|
@@ -4,6 +4,7 @@
|
||||
"accept": "Elfogadás",
|
||||
"add": "Hozzáadás",
|
||||
"add_administrator": "Adminisztrátor hozzáadása",
|
||||
"add_note": "Jegyzet hozzáadása",
|
||||
"add_relationship": "Kapcsolat hozzáadása",
|
||||
"address": "Cím",
|
||||
"admin": "Adminisztrátor",
|
||||
@@ -157,6 +158,7 @@
|
||||
"until": "-ig",
|
||||
"vaccination": "Oltás",
|
||||
"vegetable": "Zöldség",
|
||||
"verified": "Igazolt",
|
||||
"video": "Videó",
|
||||
"website": "Weboldal",
|
||||
"weight": "Súly",
|
||||
|
@@ -15,13 +15,13 @@
|
||||
let {
|
||||
closeModal,
|
||||
editProfile = () => {},
|
||||
onChange = () => {},
|
||||
removePersonFromGraph = () => {},
|
||||
addRelationship = () => {},
|
||||
createProfile = () => {},
|
||||
createRelationshipAndProfile = () => {}
|
||||
} = $props<{
|
||||
closeModal: () => void;
|
||||
onChange?: () => void;
|
||||
removePersonFromGraph?: (id: any) => void;
|
||||
addRelationship?: (id: number) => void;
|
||||
createRelationshipAndProfile?: (id: number) => void;
|
||||
editProfile?: (id: number) => void;
|
||||
@@ -65,7 +65,7 @@
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
onChange();
|
||||
removePersonFromGraph(id);
|
||||
managed_profiles_list.forEach((profile) => {
|
||||
if (profile.id === id) {
|
||||
profile.label = ['DeletedPerson'];
|
||||
@@ -90,7 +90,6 @@
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
onChange();
|
||||
managed_profiles_list = managed_profiles_list.filter((profile) => profile.id !== id);
|
||||
return;
|
||||
} else {
|
||||
@@ -105,8 +104,8 @@
|
||||
|
||||
<div class="modal modal-open z-8">
|
||||
<div class="modal-box w-full max-w-xl gap-4">
|
||||
<div class="bg-base-100 sticky top-0 z-5">
|
||||
<ModalButtons onClose={closeModal} {createProfile} />
|
||||
<div class="bg-base-100 z-5 sticky top-0">
|
||||
<ModalButtons onClose={closeModal} createProfile={()=>{createProfile();closeModal()}} />
|
||||
<div class="divider"></div>
|
||||
</div>
|
||||
<ul class="list bg-base-100 rounded-box shadow-md">
|
||||
@@ -143,14 +142,6 @@
|
||||
{create_relationship_and_person()}
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
class="btn btn-secondary btn-sm"
|
||||
onclick={() => {
|
||||
editProfile(profile.id!);
|
||||
}}
|
||||
>
|
||||
{edit()}
|
||||
</button>
|
||||
{#if profile.label?.includes('DeletedPerson')}
|
||||
<button
|
||||
class="btn btn-error btn-sm"
|
||||
@@ -161,12 +152,18 @@
|
||||
{hard_delete()}
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
class="btn btn-secondary btn-sm"
|
||||
onclick={() => {
|
||||
editProfile(profile.id!);
|
||||
}}>
|
||||
{edit()}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-error btn-sm"
|
||||
onclick={() => {
|
||||
deleteProfile(profile.id!);
|
||||
}}
|
||||
>
|
||||
}}>
|
||||
{delete_profile()}
|
||||
</button>
|
||||
{/if}
|
||||
|
@@ -1,98 +1,141 @@
|
||||
<script lang="ts">
|
||||
import type { components } from '$lib/api/api.gen.ts';
|
||||
import { child, spouse, parent, sibling } from '$lib/paraglide/messages';
|
||||
import { getBezierPath, BaseEdge, type EdgeProps, Position } from '@xyflow/svelte';
|
||||
|
||||
let {
|
||||
sourceX,
|
||||
sourceY,
|
||||
source,
|
||||
sourcePosition,
|
||||
target,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
markerEnd,
|
||||
style,
|
||||
data
|
||||
}: EdgeProps = $props();
|
||||
|
||||
let edgeType = (
|
||||
data as components['schemas']['FamilyRelationship'] & { type: string }
|
||||
).type.toLowerCase();
|
||||
let edgeLabel: string = $state(edgeType);
|
||||
let edgeColor: string;
|
||||
let srcPos;
|
||||
let tgtPos;
|
||||
if (edgeType === 'spouse') {
|
||||
edgeColor = 'stroke: red;';
|
||||
edgeLabel = spouse();
|
||||
if (sourceX < targetX) {
|
||||
tgtPos = Position.Right;
|
||||
srcPos = Position.Left;
|
||||
} else {
|
||||
tgtPos = Position.Left;
|
||||
srcPos = Position.Right;
|
||||
}
|
||||
} else if (edgeType === 'child') {
|
||||
edgeColor = 'stroke: blue;';
|
||||
edgeLabel = child();
|
||||
if (sourceY < targetY) {
|
||||
tgtPos = Position.Bottom;
|
||||
srcPos = Position.Top;
|
||||
} else {
|
||||
tgtPos = Position.Bottom;
|
||||
srcPos = Position.Top;
|
||||
}
|
||||
} else if (edgeType === 'parent') {
|
||||
edgeColor = 'stroke: green;';
|
||||
edgeLabel = parent();
|
||||
if (sourceY < targetY) {
|
||||
tgtPos = Position.Bottom;
|
||||
srcPos = Position.Top;
|
||||
} else {
|
||||
tgtPos = Position.Bottom;
|
||||
srcPos = Position.Top;
|
||||
}
|
||||
} else if (edgeType === 'sibling') {
|
||||
edgeColor = 'stroke: brown;';
|
||||
edgeLabel = sibling();
|
||||
if (sourceX < targetX) {
|
||||
tgtPos = Position.Right;
|
||||
srcPos = Position.Left;
|
||||
} else {
|
||||
tgtPos = Position.Left;
|
||||
srcPos = Position.Right;
|
||||
}
|
||||
} else {
|
||||
edgeColor = 'stroke: gray;';
|
||||
edgeLabel = edgeType;
|
||||
}
|
||||
|
||||
let [path, labelX, labelY] = $derived(
|
||||
getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition: srcPos,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition: tgtPos
|
||||
})
|
||||
);
|
||||
|
||||
edgeColor = edgeColor + 'stroke-opacity:unset; stroke-width=20;' + (style ?? '');
|
||||
|
||||
const onEdgeClick = () => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('edge-click', {
|
||||
detail: {
|
||||
start: source,
|
||||
end: target,
|
||||
data: data as components['schemas']['FamilyRelationship'] & { type: string }
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
import type { components } from '$lib/api/api.gen.ts';
|
||||
import { child, spouse, parent, sibling } from '$lib/paraglide/messages';
|
||||
import { getSmoothStepPath, BaseEdge, type EdgeProps, Position } from '@xyflow/svelte';
|
||||
|
||||
let {
|
||||
sourceX,
|
||||
sourceY,
|
||||
source,
|
||||
sourcePosition,
|
||||
sourceHandleId,
|
||||
target,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
targetHandleId,
|
||||
markerEnd,
|
||||
style,
|
||||
data
|
||||
}: EdgeProps = $props();
|
||||
|
||||
let edgeType = (
|
||||
data as components['schemas']['FamilyRelationship'] & { type: string }
|
||||
).type.toLowerCase();
|
||||
|
||||
let edgeLabel: string = $state(edgeType);
|
||||
let edgeColor: string = $state('stroke: gray;');
|
||||
let srcPos: Position = $state(sourcePosition || Position.Bottom);
|
||||
let tgtPos: Position = $state(targetPosition || Position.Top);
|
||||
|
||||
// Determine edge styling and positioning based on relationship type and handles
|
||||
if (edgeType === 'spouse') {
|
||||
edgeColor = 'stroke: red;';
|
||||
edgeLabel = spouse();
|
||||
// Use handle-based positioning for spouses
|
||||
if (sourceHandleId === 'spouse-right') {
|
||||
srcPos = Position.Right;
|
||||
tgtPos = Position.Left;
|
||||
} else if (sourceHandleId === 'spouse-left') {
|
||||
srcPos = Position.Left;
|
||||
tgtPos = Position.Right;
|
||||
} else {
|
||||
// Fallback to position-based logic
|
||||
if (sourceX < targetX) {
|
||||
srcPos = Position.Right;
|
||||
tgtPos = Position.Left;
|
||||
} else {
|
||||
srcPos = Position.Left;
|
||||
tgtPos = Position.Right;
|
||||
}
|
||||
}
|
||||
} else if (edgeType === 'child') {
|
||||
edgeColor = 'stroke: blue;';
|
||||
edgeLabel = child();
|
||||
// Parent-child: from parent's bottom (child handle) to child's top (parent handle)
|
||||
srcPos = Position.Bottom;
|
||||
tgtPos = Position.Top;
|
||||
} else if (edgeType === 'parent') {
|
||||
edgeColor = 'stroke: blue;';
|
||||
edgeLabel = parent();
|
||||
// Parent relationship: from child (top) to parent (bottom)
|
||||
srcPos = Position.Top;
|
||||
tgtPos = Position.Bottom;
|
||||
} else if (edgeType === 'sibling') {
|
||||
edgeColor = 'stroke: orange;';
|
||||
edgeLabel = sibling();
|
||||
// Use handle-based positioning for siblings
|
||||
if (sourceHandleId === 'spouse-right') {
|
||||
srcPos = Position.Right;
|
||||
tgtPos = Position.Left;
|
||||
} else if (sourceHandleId === 'spouse-left') {
|
||||
srcPos = Position.Left;
|
||||
tgtPos = Position.Right;
|
||||
} else {
|
||||
// Fallback to position-based logic
|
||||
if (sourceX < targetX) {
|
||||
srcPos = Position.Right;
|
||||
tgtPos = Position.Left;
|
||||
} else {
|
||||
srcPos = Position.Left;
|
||||
tgtPos = Position.Right;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
edgeColor = 'stroke: gray;';
|
||||
edgeLabel = edgeType;
|
||||
// Keep original positions for unknown types
|
||||
srcPos = sourcePosition || Position.Bottom;
|
||||
tgtPos = targetPosition || Position.Top;
|
||||
}
|
||||
|
||||
// Explicit handle overrides (these should take precedence)
|
||||
if (sourceHandleId === 'child') {
|
||||
srcPos = Position.Bottom;
|
||||
}
|
||||
if (targetHandleId === 'parent') {
|
||||
tgtPos = Position.Top;
|
||||
}
|
||||
if (sourceHandleId === 'spouse-left') {
|
||||
srcPos = Position.Left;
|
||||
}
|
||||
if (sourceHandleId === 'spouse-right') {
|
||||
srcPos = Position.Right;
|
||||
}
|
||||
if (targetHandleId === 'spouse-left') {
|
||||
tgtPos = Position.Left;
|
||||
}
|
||||
if (targetHandleId === 'spouse-right') {
|
||||
tgtPos = Position.Right;
|
||||
}
|
||||
|
||||
let [path, labelX, labelY] = $derived(
|
||||
getSmoothStepPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition: srcPos,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition: tgtPos,
|
||||
borderRadius: 20, // Add some rounding to make paths smoother
|
||||
offset: 20 // Add offset to avoid overlapping with nodes
|
||||
})
|
||||
);
|
||||
|
||||
// Fix the style string formatting
|
||||
const finalStyle = `${edgeColor} stroke-width: 3; stroke-opacity: 0.8; ${style ?? ''}`;
|
||||
|
||||
const onEdgeClick = () => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('edge-click', {
|
||||
detail: {
|
||||
start: source,
|
||||
end: target,
|
||||
data: data as components['schemas']['FamilyRelationship'] & { type: string }
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
</script>
|
||||
|
||||
<BaseEdge {path} {labelX} {labelY} {markerEnd} style={edgeColor} onclick={onEdgeClick} />
|
||||
<BaseEdge {path} {labelX} {labelY} {markerEnd} style={finalStyle} onclick={onEdgeClick} />
|
@@ -17,7 +17,7 @@
|
||||
export let deleteNode: () => void;
|
||||
export let createRelationshipAndNode: () => void;
|
||||
export let addRelationship: () => void;
|
||||
export let addAdmin: (() => void) | undefined;
|
||||
// export let addAdmin: (() => void) | undefined;
|
||||
|
||||
let contextMenu: HTMLDivElement;
|
||||
let isAdmin: boolean = false;
|
||||
|
@@ -26,6 +26,38 @@
|
||||
class={'card card-compact flex h-40 w-40 flex-col items-center justify-center rounded-full shadow-lg' +
|
||||
nodeColor}
|
||||
>
|
||||
<Handle
|
||||
class="customHandle"
|
||||
id="spouse-left"
|
||||
{isValidConnection}
|
||||
position={Position.Left}
|
||||
isConnectable={true}
|
||||
type="source"
|
||||
/>
|
||||
<Handle
|
||||
class="customHandle"
|
||||
id="spouse-right"
|
||||
{isValidConnection}
|
||||
position={Position.Right}
|
||||
isConnectable={true}
|
||||
type="source"
|
||||
/>
|
||||
<Handle
|
||||
class="customHandle"
|
||||
id="spouse-left"
|
||||
{isValidConnection}
|
||||
position={Position.Left}
|
||||
isConnectable={true}
|
||||
type="target"
|
||||
/>
|
||||
<Handle
|
||||
class="customHandle"
|
||||
id="spouse-right"
|
||||
{isValidConnection}
|
||||
position={Position.Right}
|
||||
isConnectable={true}
|
||||
type="target"
|
||||
/>
|
||||
<Handle
|
||||
class="customHandle"
|
||||
id="child"
|
||||
@@ -81,7 +113,7 @@
|
||||
/>
|
||||
|
||||
<div class="avatar mb-2" style="z-index: 2; cursor: pointer;">
|
||||
<div class={"w-24 rounded-full border-0 ring-offset-1"+nodeColor}>
|
||||
<div class={'w-24 rounded-full border-0 ring-offset-1' + nodeColor}>
|
||||
<img
|
||||
src={data.profile_picture || 'https://cdn-icons-png.flaticon.com/512/10628/10628885.png'}
|
||||
alt="Picture of {data.last_name} {data.first_name}"
|
||||
@@ -90,7 +122,7 @@
|
||||
</div>
|
||||
|
||||
<div class="px-2 text-center" style="z-index: 2; cursor: pointer;">
|
||||
<h2 class="text-sm leading-tight font-semibold">
|
||||
<h2 class="text-sm font-semibold leading-tight">
|
||||
{data.first_name}
|
||||
{data.middle_name ? data.middle_name : ''}
|
||||
{data.last_name}
|
||||
|
@@ -4,137 +4,229 @@ import type { Edge, Node } from '@xyflow/svelte';
|
||||
import { Position } from '@xyflow/svelte';
|
||||
|
||||
export class FamilyTree extends dagre.graphlib.Graph {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
getLayoutedElements(
|
||||
nodes: Node[],
|
||||
edges: Edge[],
|
||||
nodeWidth: number,
|
||||
nodeHeight: number,
|
||||
direction = 'TB'
|
||||
): Layout {
|
||||
this.setGraph({ rankdir: direction });
|
||||
this.setDefaultEdgeLabel(() => ({}));
|
||||
nodes.forEach((node) => {
|
||||
this.setNode(node.id, { width: nodeWidth, height: nodeHeight });
|
||||
});
|
||||
getLayoutedElements(
|
||||
nodes: Node[],
|
||||
edges: Edge[],
|
||||
nodeWidth: number,
|
||||
nodeHeight: number,
|
||||
direction = 'TB'
|
||||
): Layout {
|
||||
this.setGraph({
|
||||
rankdir: direction,
|
||||
nodesep: 80,
|
||||
ranksep: 120,
|
||||
marginx: 50,
|
||||
marginy: 50
|
||||
});
|
||||
this.setDefaultEdgeLabel(() => ({}));
|
||||
|
||||
edges.forEach((edge) => {
|
||||
if (String(edge.data!.type).toLowerCase() === 'child') {
|
||||
this.setEdge(edge.source, edge.target);
|
||||
}
|
||||
});
|
||||
// Add nodes to dagre
|
||||
nodes.forEach((node) => {
|
||||
this.setNode(node.id, { width: nodeWidth, height: nodeHeight });
|
||||
});
|
||||
|
||||
dagre.layout(this);
|
||||
// Only add parent-child edges to dagre for hierarchical layout
|
||||
edges.forEach((edge) => {
|
||||
if (String(edge.data!.type).toLowerCase() === 'child') {
|
||||
this.setEdge(edge.source, edge.target);
|
||||
}
|
||||
});
|
||||
|
||||
let newEdges: Edge[] = [];
|
||||
edges.forEach((edge) => {
|
||||
let newEdge = { ...edge };
|
||||
if (String(edge.data?.type).toLowerCase() === 'child') {
|
||||
newEdge.sourceHandle = 'child';
|
||||
newEdge.targetHandle = 'parent';
|
||||
} else if (String(edge.data?.type).toLowerCase() === 'parent') {
|
||||
return;
|
||||
}
|
||||
// Run dagre layout
|
||||
dagre.layout(this);
|
||||
|
||||
const sourceNode = this.node(edge.source);
|
||||
const targetNode = this.node(edge.target);
|
||||
if (!sourceNode || !targetNode) {
|
||||
return;
|
||||
}
|
||||
// Create maps for relationship analysis
|
||||
const spouseMap = new Map<string, string>();
|
||||
const siblingMap = new Map<string, string[]>();
|
||||
const childrenMap = new Map<string, string[]>();
|
||||
const parentsMap = new Map<string, string[]>();
|
||||
|
||||
if (String(edge.data?.type).toLowerCase() === 'sibling') {
|
||||
const padding = 50; // distance between sibling and source
|
||||
const spouseWidth = nodeWidth;
|
||||
// Build relationship maps
|
||||
edges.forEach((edge) => {
|
||||
const type = String(edge.data?.type).toLowerCase();
|
||||
if (type === 'spouse') {
|
||||
spouseMap.set(edge.source, edge.target);
|
||||
spouseMap.set(edge.target, edge.source);
|
||||
} else if (type === 'sibling') {
|
||||
if (!siblingMap.has(edge.source)) siblingMap.set(edge.source, []);
|
||||
if (!siblingMap.has(edge.target)) siblingMap.set(edge.target, []);
|
||||
siblingMap.get(edge.source)!.push(edge.target);
|
||||
siblingMap.get(edge.target)!.push(edge.source);
|
||||
} else if (type === 'child') {
|
||||
if (!childrenMap.has(edge.source)) childrenMap.set(edge.source, []);
|
||||
if (!parentsMap.has(edge.target)) parentsMap.set(edge.target, []);
|
||||
childrenMap.get(edge.source)!.push(edge.target);
|
||||
parentsMap.get(edge.target)!.push(edge.source);
|
||||
}
|
||||
});
|
||||
|
||||
const existingNodesAtLevel = nodes
|
||||
.map((n) => ({ id: n.id, pos: this.node(n.id) }))
|
||||
.filter(({ pos }) => Math.abs(pos.y - sourceNode.y) < nodeHeight / 2); // same horizontal band
|
||||
// Helper function to check if position is occupied
|
||||
const isPositionOccupied = (x: number, y: number, excludeId?: string): boolean => {
|
||||
return nodes.some(node => {
|
||||
if (excludeId && node.id === excludeId) return false;
|
||||
const nodePos = this.node(node.id);
|
||||
return Math.abs(nodePos.x - x) < nodeWidth && Math.abs(nodePos.y - y) < nodeHeight;
|
||||
});
|
||||
};
|
||||
|
||||
// Collect taken x ranges
|
||||
const takenXRanges = existingNodesAtLevel.map(({ pos }) => ({
|
||||
from: pos.x - spouseWidth / 2,
|
||||
to: pos.x + spouseWidth / 2
|
||||
}));
|
||||
// Helper function to find free position near a reference point
|
||||
const findFreePosition = (refX: number, refY: number, excludeId?: string): { x: number, y: number } => {
|
||||
const padding = 30;
|
||||
const positions = [
|
||||
{ x: refX + nodeWidth + padding, y: refY }, // right
|
||||
{ x: refX - nodeWidth - padding, y: refY }, // left
|
||||
{ x: refX + (nodeWidth + padding) * 2, y: refY }, // far right
|
||||
{ x: refX - (nodeWidth + padding) * 2, y: refY }, // far left
|
||||
];
|
||||
|
||||
// Try placing spouse to the right
|
||||
let desiredX = sourceNode.x + nodeWidth + padding;
|
||||
for (const pos of positions) {
|
||||
if (!isPositionOccupied(pos.x, pos.y, excludeId)) {
|
||||
return pos;
|
||||
}
|
||||
}
|
||||
|
||||
// If all positions are taken, just go further right
|
||||
return { x: refX + (nodeWidth + padding) * 3, y: refY };
|
||||
};
|
||||
|
||||
// Check for collision
|
||||
const collides = (x: number) => {
|
||||
return takenXRanges.some(({ from, to }) => x > from && x < to);
|
||||
};
|
||||
// Position spouses next to each other
|
||||
const processedSpouses = new Set<string>();
|
||||
spouseMap.forEach((spouse, person) => {
|
||||
if (processedSpouses.has(person)) return;
|
||||
|
||||
const personNode = this.node(person);
|
||||
const spouseNode = this.node(spouse);
|
||||
|
||||
// Determine who should be the anchor (prefer the one with children or hierarchically positioned)
|
||||
const personHasChildren = childrenMap.has(person) && childrenMap.get(person)!.length > 0;
|
||||
const spouseHasChildren = childrenMap.has(spouse) && childrenMap.get(spouse)!.length > 0;
|
||||
|
||||
let anchorNode, mobileNode, anchorId, mobileId;
|
||||
if (personHasChildren && !spouseHasChildren) {
|
||||
anchorNode = personNode;
|
||||
mobileNode = spouseNode;
|
||||
anchorId = person;
|
||||
mobileId = spouse;
|
||||
} else if (!personHasChildren && spouseHasChildren) {
|
||||
anchorNode = spouseNode;
|
||||
mobileNode = personNode;
|
||||
anchorId = spouse;
|
||||
mobileId = person;
|
||||
} else {
|
||||
// Both or neither have children, use alphabetical order
|
||||
if (person < spouse) {
|
||||
anchorNode = personNode;
|
||||
mobileNode = spouseNode;
|
||||
anchorId = person;
|
||||
mobileId = spouse;
|
||||
} else {
|
||||
anchorNode = spouseNode;
|
||||
mobileNode = personNode;
|
||||
anchorId = spouse;
|
||||
mobileId = person;
|
||||
}
|
||||
}
|
||||
|
||||
// If right side collides, try left
|
||||
if (collides(desiredX)) {
|
||||
desiredX = sourceNode.x - (nodeWidth + padding);
|
||||
}
|
||||
// Position mobile spouse next to anchor
|
||||
const newPos = findFreePosition(anchorNode.x, anchorNode.y, mobileId);
|
||||
mobileNode.x = newPos.x;
|
||||
mobileNode.y = newPos.y;
|
||||
|
||||
// If both sides collide, push right until free
|
||||
while (collides(desiredX)) {
|
||||
desiredX += nodeWidth + padding;
|
||||
}
|
||||
processedSpouses.add(person);
|
||||
processedSpouses.add(spouse);
|
||||
});
|
||||
|
||||
targetNode.x = desiredX;
|
||||
targetNode.y = sourceNode.y;
|
||||
}
|
||||
// Position siblings
|
||||
const processedSiblings = new Set<string>();
|
||||
siblingMap.forEach((siblings, person) => {
|
||||
if (processedSiblings.has(person)) return;
|
||||
|
||||
const personNode = this.node(person);
|
||||
|
||||
siblings.forEach((sibling) => {
|
||||
if (processedSiblings.has(sibling)) return;
|
||||
|
||||
const siblingNode = this.node(sibling);
|
||||
|
||||
// If sibling is not a spouse of someone at the same level, position as sibling
|
||||
const siblingSpouse = spouseMap.get(sibling);
|
||||
if (!siblingSpouse || Math.abs(this.node(siblingSpouse).y - personNode.y) > nodeHeight) {
|
||||
const newPos = findFreePosition(personNode.x, personNode.y, sibling);
|
||||
siblingNode.x = newPos.x;
|
||||
siblingNode.y = newPos.y;
|
||||
}
|
||||
|
||||
processedSiblings.add(sibling);
|
||||
});
|
||||
|
||||
processedSiblings.add(person);
|
||||
});
|
||||
|
||||
if (String(edge.data?.type).toLowerCase() === 'spouse') {
|
||||
const padding = 50; // distance between spouse and source
|
||||
const spouseWidth = nodeWidth;
|
||||
// Create new edges
|
||||
let newEdges: Edge[] = [];
|
||||
const processedSpouseEdges = new Set<string>();
|
||||
|
||||
edges.forEach((edge) => {
|
||||
let newEdge = { ...edge };
|
||||
|
||||
if (String(edge.data?.type).toLowerCase() === 'child') {
|
||||
// Parent to child: source (parent) uses 'child' handle (bottom), target (child) uses 'parent' handle (top)
|
||||
newEdge.sourceHandle = 'child';
|
||||
newEdge.targetHandle = 'parent';
|
||||
} else if (String(edge.data?.type).toLowerCase() === 'parent') {
|
||||
return;
|
||||
} else if (String(edge.data?.type).toLowerCase() === 'spouse') {
|
||||
// Avoid duplicate spouse edges by creating a unique key
|
||||
const spouseKey = [edge.source, edge.target].sort().join('-');
|
||||
if (processedSpouseEdges.has(spouseKey)) {
|
||||
return; // Skip this duplicate spouse edge
|
||||
}
|
||||
processedSpouseEdges.add(spouseKey);
|
||||
|
||||
// Set spouse handles based on position
|
||||
const sourceNode = this.node(edge.source);
|
||||
const targetNode = this.node(edge.target);
|
||||
if (sourceNode.x < targetNode.x) {
|
||||
newEdge.sourceHandle = 'spouse-right';
|
||||
newEdge.targetHandle = 'spouse-left';
|
||||
} else {
|
||||
newEdge.sourceHandle = 'spouse-left';
|
||||
newEdge.targetHandle = 'spouse-right';
|
||||
}
|
||||
} else if (String(edge.data?.type).toLowerCase() === 'sibling') {
|
||||
// Set sibling handles based on position
|
||||
const sourceNode = this.node(edge.source);
|
||||
const targetNode = this.node(edge.target);
|
||||
if (sourceNode.x < targetNode.x) {
|
||||
newEdge.sourceHandle = 'spouse-right';
|
||||
newEdge.targetHandle = 'spouse-left';
|
||||
} else {
|
||||
newEdge.sourceHandle = 'spouse-left';
|
||||
newEdge.targetHandle = 'spouse-right';
|
||||
}
|
||||
}
|
||||
|
||||
const existingNodesAtLevel = nodes
|
||||
.map((n) => ({ id: n.id, pos: this.node(n.id) }))
|
||||
.filter(({ pos }) => Math.abs(pos.y - sourceNode.y) < nodeHeight / 2); // same horizontal band
|
||||
newEdge.hidden = false;
|
||||
newEdge.type = 'familyEdge';
|
||||
newEdges.push(newEdge);
|
||||
});
|
||||
|
||||
// Collect taken x ranges
|
||||
const takenXRanges = existingNodesAtLevel.map(({ pos }) => ({
|
||||
from: pos.x - spouseWidth / 2,
|
||||
to: pos.x + spouseWidth / 2
|
||||
}));
|
||||
const layoutedNodes = nodes.map((node) => {
|
||||
const nodeWithPosition = this.node(node.id);
|
||||
return {
|
||||
...node,
|
||||
type: 'personNode',
|
||||
position: {
|
||||
x: nodeWithPosition.x - nodeWidth / 2,
|
||||
y: nodeWithPosition.y - nodeHeight / 2
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// Try placing spouse to the right
|
||||
let desiredX = sourceNode.x + nodeWidth + padding;
|
||||
|
||||
// Check for collision
|
||||
const collides = (x: number) => {
|
||||
return takenXRanges.some(({ from, to }) => x > from && x < to);
|
||||
};
|
||||
|
||||
// If right side collides, try left
|
||||
if (collides(desiredX)) {
|
||||
desiredX = sourceNode.x - (nodeWidth + padding);
|
||||
}
|
||||
|
||||
// If both sides collide, push right until free
|
||||
while (collides(desiredX)) {
|
||||
desiredX += nodeWidth + padding;
|
||||
}
|
||||
|
||||
targetNode.x = desiredX;
|
||||
targetNode.y = sourceNode.y;
|
||||
}
|
||||
newEdge.hidden = false;
|
||||
newEdge.type = 'familyEdge';
|
||||
|
||||
newEdges.push(newEdge);
|
||||
});
|
||||
|
||||
const layoutedNodes = nodes.map((node) => {
|
||||
const nodeWithPosition = this.node(node.id);
|
||||
|
||||
return {
|
||||
...node,
|
||||
type: 'personNode',
|
||||
position: {
|
||||
x: nodeWithPosition.x - nodeWidth / 2,
|
||||
y: nodeWithPosition.y - nodeHeight / 2
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
return { Nodes: layoutedNodes, Edges: newEdges };
|
||||
}
|
||||
}
|
||||
return { Nodes: layoutedNodes, Edges: newEdges };
|
||||
}
|
||||
}
|
@@ -4,7 +4,8 @@
|
||||
description,
|
||||
life_events,
|
||||
unknown,
|
||||
until
|
||||
until,
|
||||
remove
|
||||
} from '$lib/paraglide/messages';
|
||||
import type { components } from '$lib/api/api.gen';
|
||||
|
||||
@@ -23,10 +24,16 @@
|
||||
}
|
||||
|
||||
function addEvent() {
|
||||
const newEvent = { from: '', to: '', description: '' };
|
||||
const newEvent = { from: '', to: undefined, description: '' };
|
||||
person_life_events = [...(person_life_events ?? []), newEvent];
|
||||
onChange('life_events', person_life_events);
|
||||
}
|
||||
|
||||
function removeEvent(index: number) {
|
||||
if (!person_life_events) return;
|
||||
person_life_events = person_life_events.filter((_, i) => i !== index);
|
||||
onChange('life_events', person_life_events);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if person_life_events?.length}
|
||||
@@ -34,15 +41,25 @@
|
||||
<ul class="timeline timeline-snap-start timeline-vertical">
|
||||
{#each person_life_events as event, index}
|
||||
<li>
|
||||
<div class="timeline-start">
|
||||
<div class="timeline-start flex items-center">
|
||||
{#if editorMode}
|
||||
<input
|
||||
type="text"
|
||||
type="date"
|
||||
class="input input-xs input-bordered"
|
||||
value={event.from ?? ''}
|
||||
on:input={(e) => updateEvent(index, 'from', e.currentTarget.value)}
|
||||
placeholder={unknown().toLowerCase()}
|
||||
/>
|
||||
<!-- Remove button -->
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-xs btn-ghost text-error ml-2"
|
||||
title={remove()}
|
||||
on:click={() => removeEvent(index)}
|
||||
aria-label={remove() + ' ' + life_events()}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
{:else}
|
||||
{event.from ?? unknown().toLowerCase()}
|
||||
{/if}
|
||||
@@ -69,7 +86,7 @@
|
||||
{until()}
|
||||
{#if editorMode}
|
||||
<input
|
||||
type="text"
|
||||
type="date"
|
||||
class="input input-xs input-bordered ml-1"
|
||||
value={event.to ?? ''}
|
||||
on:input={(e) => updateEvent(index, 'to', e.currentTarget.value)}
|
||||
|
@@ -30,7 +30,9 @@
|
||||
) {
|
||||
draftPerson[field] = value;
|
||||
if (field === 'invite_code') {
|
||||
save();
|
||||
save().then(() => {
|
||||
editorMode = true;
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -47,6 +49,7 @@
|
||||
|
||||
async function save() {
|
||||
try {
|
||||
console.debug('Saving person data:', draftPerson);
|
||||
const response = await fetch(`/api/person/${person.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
@@ -56,7 +59,8 @@
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
alert('Error saving person data, status: ' + response.status);
|
||||
console.error('Error saving person data, status: ', response.status, (await response.json()));
|
||||
alert('Error saving person data, status: ' + response.status + (await response.json()));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -67,12 +71,8 @@
|
||||
};
|
||||
} else {
|
||||
const errorDetails = await response.json();
|
||||
alert(
|
||||
'Error saving person data, status: ' +
|
||||
response.status +
|
||||
' ' +
|
||||
JSON.stringify(errorDetails)
|
||||
);
|
||||
console.error('Error details:', errorDetails);
|
||||
alert(`Error saving person data, status: ${response.status} ${JSON.stringify(errorDetails)}`);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('An unexpected error occurred: ' + error);
|
||||
@@ -82,8 +82,8 @@
|
||||
</script>
|
||||
|
||||
<div class="modal modal-open" transition:fade>
|
||||
<div class="modal-box max-h-screen w-full max-h-80 max-w-5xl overflow-y-auto">
|
||||
<div class="bg-base-100 sticky top-0 z-7">
|
||||
<div class="modal-box max-h-80 max-h-screen w-full max-w-5xl overflow-y-auto">
|
||||
<div class="bg-base-100 z-7 sticky top-0">
|
||||
<ModalButtons {editorMode} onClose={close} onSave={save} onToggleEdit={toggleEdit} />
|
||||
<div class="divider"></div>
|
||||
</div>
|
||||
|
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { callMessageFunction } from '$lib/i18n';
|
||||
import type { MessageKeys } from '$lib/i18n';
|
||||
import { add_note, notes, theme } from '$lib/paraglide/messages';
|
||||
import type { components } from '$lib/api/api.gen';
|
||||
|
||||
export let person: components['schemas']['PersonProperties'];
|
||||
@@ -29,7 +30,8 @@
|
||||
'notes',
|
||||
'phone',
|
||||
'audios',
|
||||
'google_id'
|
||||
'google_id',
|
||||
'invite_code'
|
||||
];
|
||||
let newNote = {
|
||||
title: " ",
|
||||
@@ -37,15 +39,60 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="mt-6 grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
{#each person.notes??[] as note}
|
||||
<div class="card bg-base-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">{note.title}</h2>
|
||||
<p>{note.note}</p>
|
||||
<div class="mt-5 flex flex-col gap-2 justify-center items-center">
|
||||
{#each person.notes ?? [] as note, i}
|
||||
<div class="card bg-base-100 shadow-sm relative w-full max-w-xl">
|
||||
<div class="card-body p-4 w-full">
|
||||
{#if editorMode}
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered input-sm w-full mb-2"
|
||||
placeholder={theme()}
|
||||
bind:value={note.title}
|
||||
oninput={() => onChange('notes', person.notes)}
|
||||
/>
|
||||
<textarea
|
||||
class="textarea textarea-bordered textarea-sm w-full"
|
||||
placeholder={notes()}
|
||||
bind:value={note.note}
|
||||
oninput={() => onChange('notes', person.notes)}
|
||||
></textarea>
|
||||
<button
|
||||
type="button"
|
||||
class="absolute top-2 right-2 btn btn-xs btn-ghost text-error ml-2"
|
||||
aria-label="Remove note"
|
||||
onclick={() => {
|
||||
person.notes = (person.notes ?? []).filter((_, idx) => idx !== i);
|
||||
onChange('notes', person.notes);
|
||||
}}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
{:else}
|
||||
<h2 class="card-title">{note.title}</h2>
|
||||
<p class="text-sm text-gray-500">{note.date}</p>
|
||||
<p>{note.note}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{#if editorMode}
|
||||
<button
|
||||
class="btn btn-accent btn-sm w-auto self-start"
|
||||
onclick={() => {
|
||||
const now = new Date();
|
||||
const formattedDate = now.getFullYear() + '-' +
|
||||
String(now.getMonth() + 1).padStart(2, '0') + '-' +
|
||||
String(now.getDate()).padStart(2, '0');
|
||||
person.notes = [...(person.notes ?? []), { title: '', note: '', date: formattedDate }];
|
||||
onChange('notes', person.notes);
|
||||
}}
|
||||
>
|
||||
{add_note()}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="mt-6 grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
{#each Object.entries(person) as [key, value]}
|
||||
{#if !skipFields.includes(key) && ((value !== undefined && value !== null) || editorMode)}
|
||||
<div>
|
||||
|
@@ -2,6 +2,7 @@
|
||||
import { fade } from 'svelte/transition';
|
||||
import {
|
||||
create,
|
||||
create_person,
|
||||
close,
|
||||
born,
|
||||
mothers_first_name,
|
||||
@@ -115,7 +116,8 @@
|
||||
let newNode = {
|
||||
id: "person"+String(data.person?.Id),
|
||||
data: {
|
||||
...data.person?.Props
|
||||
...data.person?.Props,
|
||||
id: data.person?.Id,
|
||||
},
|
||||
position: { x: 0, y: 0 },
|
||||
type: 'personNode'
|
||||
@@ -205,7 +207,7 @@
|
||||
<div class="modal modal-open max-h-screen" transition:fade>
|
||||
<div class="modal-box flex w-full max-w-5xl flex-col items-center justify-center overflow-y-auto">
|
||||
<div class="flex w-full max-w-5xl items-center justify-between p-2">
|
||||
<h3 class="text-left text-lg font-bold">{create_relationship_and_person()}</h3>
|
||||
<h3 class="text-left text-lg font-bold">{relationshipStartID !== null?create_relationship_and_person():create_person()}</h3>
|
||||
<div>
|
||||
<button class="btn btn-error btn-sm" onclick={onClose}>
|
||||
{close()}
|
||||
|
@@ -7,7 +7,7 @@
|
||||
parent,
|
||||
relation,
|
||||
relation_type,
|
||||
relationship,
|
||||
verified,
|
||||
sibling,
|
||||
spouse,
|
||||
until
|
||||
@@ -131,11 +131,12 @@
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.log('Cannot create relationship' + ', status: ' + response.status);
|
||||
console.error('Cannot create relationship' + ', status: ' + response.status + (await response.json()));
|
||||
return;
|
||||
}
|
||||
|
||||
const created = (await response.json()) as components['schemas']['dbtypeRelationship'][];
|
||||
console.debug('Relationship created successfully',created);
|
||||
relationships.push(...created);
|
||||
|
||||
let newEdges: Edge[] = [];
|
||||
@@ -149,6 +150,7 @@
|
||||
});
|
||||
}
|
||||
onCreation(newEdges);
|
||||
closeModal();
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -187,6 +189,14 @@
|
||||
</div>
|
||||
{/if}
|
||||
{#if !createRelationship}
|
||||
<div class="form-control mt-2">
|
||||
<p>
|
||||
<strong>{id()} 1:</strong> {startNode}
|
||||
</p>
|
||||
<p>
|
||||
<strong>{id()} 2:</strong> {endNode}
|
||||
</p>
|
||||
</div>
|
||||
<!-- Editor mode: show all existing relationships -->
|
||||
{#each relationships as r, index}
|
||||
<div class="border-base-300 mt-4 rounded border p-4">
|
||||
@@ -203,7 +213,7 @@
|
||||
class="checkbox"
|
||||
/>
|
||||
{:else}
|
||||
<p><strong>Verified:</strong>{r.Props?.verified}</p>
|
||||
<p><strong>{verified()}:</strong>{r.Props?.verified}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="form-control mt-2">
|
||||
|
@@ -54,6 +54,12 @@
|
||||
|
||||
let clientWidth: number | undefined = $state();
|
||||
let clientHeight: number | undefined = $state();
|
||||
|
||||
let removePersonFromGraph = (id: any) => {
|
||||
nodes = nodes.filter((n) => n.data.id !== id);
|
||||
edges = edges.filter((e) => e.source !== 'person' + id && e.target !== 'person' + id);
|
||||
};
|
||||
|
||||
let delete_profile = (id: any) => {
|
||||
fetch('/api/person/' + id, {
|
||||
method: 'DELETE',
|
||||
@@ -63,8 +69,7 @@
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
nodes = nodes.filter((n) => n.data.id !== id);
|
||||
edges = edges.filter((e) => e.source !== 'person' + id && e.target !== 'person' + id);
|
||||
removePersonFromGraph(id);
|
||||
} else {
|
||||
alert('Error deleting person');
|
||||
}
|
||||
@@ -149,7 +154,7 @@
|
||||
);
|
||||
edges = [...newLayout.Edges];
|
||||
nodes = [...newLayout.Nodes];
|
||||
};
|
||||
}
|
||||
|
||||
let handleNodeClickFunc = handleNodeClick(
|
||||
(
|
||||
@@ -157,8 +162,8 @@
|
||||
id: number | undefined;
|
||||
}
|
||||
) => {
|
||||
openPersonPanel = true;
|
||||
selectedPerson = { ...person, id: String(person.id) };
|
||||
openPersonPanel = true;
|
||||
fetch('/api/person/' + person.id, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
@@ -180,6 +185,7 @@
|
||||
};
|
||||
selectedPerson.id = String(person.id);
|
||||
}
|
||||
console.debug('Fetched person data:', data);
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -273,7 +279,7 @@
|
||||
onOnlyPersonCreation={() => {
|
||||
createPerson = false;
|
||||
}}
|
||||
onCreation={(node,edges) => {
|
||||
onCreation={(node, edges) => {
|
||||
onCreation([node], edges);
|
||||
createPerson = false;
|
||||
}}
|
||||
@@ -355,19 +361,19 @@
|
||||
};
|
||||
selectedPerson.id = String(id);
|
||||
openPersonPanel = true;
|
||||
}else {
|
||||
} else {
|
||||
alert('Error fetching person data');
|
||||
}
|
||||
});
|
||||
}}
|
||||
onChange={() => {}}
|
||||
removePersonFromGraph={removePersonFromGraph}
|
||||
/>
|
||||
{/if}
|
||||
</SvelteFlow>
|
||||
</SvelteFlowProvider>
|
||||
</div>
|
||||
|
||||
<div class="absolute top-2 left-2 flex flex-row items-center gap-2">
|
||||
<div class="absolute left-2 top-2 flex flex-row items-center gap-2">
|
||||
<HamburgerIcon
|
||||
open_admin_panel={() => {
|
||||
adminMenu = !adminMenu;
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import { client } from '$lib/api/client';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { RequestEvent } from './$types';
|
||||
import { json } from 'stream/consumers';
|
||||
|
||||
export async function GET(event: RequestEvent): Promise<Response> {
|
||||
if (event.locals.session === null) {
|
||||
|
@@ -12,7 +12,7 @@ export async function POST(event: RequestEvent): Promise<Response> {
|
||||
params: {
|
||||
header: { 'X-User-ID': event.locals.session.userId }
|
||||
},
|
||||
body: event.request.json() as {
|
||||
body: (await event.request.json()) as {
|
||||
id1?: number;
|
||||
id2?: number;
|
||||
type?: 'child' | 'parent' | 'spouse' | 'sibling';
|
||||
|
Reference in New Issue
Block a user