mirror of
https://github.com/vcscsvcscs/GenerationsHeritage.git
synced 2025-08-12 13:59:08 +02:00
fixup admin platform
This commit is contained in:
@@ -3213,6 +3213,12 @@
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"label":{
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"first_name": {
|
||||
"type": "string"
|
||||
},
|
||||
@@ -3221,20 +3227,6 @@
|
||||
},
|
||||
"last_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"EndId": {
|
||||
"type": "integer"
|
||||
},
|
||||
"EndElementId": {
|
||||
"type": "string"
|
||||
},
|
||||
"Props": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"added": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@@ -42,6 +42,7 @@
|
||||
"dark": "Dark",
|
||||
"date": "Date",
|
||||
"death": "Death",
|
||||
"delete_profile":"Delete profile",
|
||||
"deceased": "Deceased",
|
||||
"deny": "Deny",
|
||||
"description": "Description",
|
||||
@@ -67,6 +68,8 @@
|
||||
"from_time": "From",
|
||||
"fruit": "Fruit",
|
||||
"hair_colour": "Hair Colour",
|
||||
"hard_delete": "Delete permanently",
|
||||
"have_invite_code": "I have invite code!",
|
||||
"hello_world": "Hello, {name} from en!",
|
||||
"height": "Height",
|
||||
"hobby": "Hobby",
|
||||
|
@@ -43,6 +43,7 @@
|
||||
"date": "Dátum",
|
||||
"death": "Halál",
|
||||
"deceased": "Elhunyt",
|
||||
"delete_profile":"Delete profile",
|
||||
"deny": "Elutasítás",
|
||||
"description": "Leírás",
|
||||
"details": "Részletek",
|
||||
@@ -67,6 +68,8 @@
|
||||
"from_time": "Tól",
|
||||
"fruit": "Gyümölcs",
|
||||
"hair_colour": "Hajszín",
|
||||
"hard_delete": "Végleges Törlés",
|
||||
"have_invite_code": "Rendelkezem meghívó kóddal!",
|
||||
"hello_world": "Helló, {name} innen: hu!",
|
||||
"height": "Magasság",
|
||||
"hobby": "Hobbi",
|
||||
|
168
apps/app/src/lib/admin/Modal.svelte
Normal file
168
apps/app/src/lib/admin/Modal.svelte
Normal file
@@ -0,0 +1,168 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
hard_delete,
|
||||
managed_profiles,
|
||||
delete_profile,
|
||||
edit,
|
||||
from_time,
|
||||
admin,
|
||||
create_relationship_and_person,
|
||||
|
||||
add_relationship
|
||||
|
||||
} from '$lib/paraglide/messages';
|
||||
import ModalButtons from './ModalButtons.svelte';
|
||||
import type { components, operations } from '$lib/api/api.gen';
|
||||
|
||||
let {
|
||||
closeModal,
|
||||
editProfile = () => {},
|
||||
onChange = () => {},
|
||||
addRelationship = () => {},
|
||||
createProfile = ()=> {},
|
||||
createRelationshipAndProfile = () => {},
|
||||
} = $props<{
|
||||
closeModal: () => void;
|
||||
onChange?: () => void;
|
||||
addRelationship?: (id: number) => void;
|
||||
createRelationshipAndProfile?: (id: number) => void;
|
||||
editProfile?: (id: number) => void;
|
||||
createProfile?: () => void;
|
||||
}>();
|
||||
|
||||
let managed_profiles_list: components['schemas']['Admin'][] = $state([]);
|
||||
function fetchManagedProfiles(){
|
||||
fetch(`/api/managed_profiles`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
console.log('Cannot get managed profiles, status: ' + response.status);
|
||||
return;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then((data) => {
|
||||
if (data) {
|
||||
managed_profiles_list = [...(data as components['schemas']['Admin'][])];
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error fetching managed profiles:', error);
|
||||
});
|
||||
}
|
||||
|
||||
fetchManagedProfiles();
|
||||
|
||||
async function deleteProfile(id: number) {
|
||||
fetch('/api/person/' + id, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
onChange();
|
||||
managed_profiles_list = managed_profiles_list.filter((profile) => profile.id !== id);
|
||||
return;
|
||||
} else {
|
||||
alert('Error deleting person');
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.info('Error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
async function hardDeleteProfile(id: number) {
|
||||
fetch('/api/person/' + id + '/hard-delete', {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.ok) {
|
||||
onChange();
|
||||
managed_profiles_list = managed_profiles_list.filter((profile) => profile.id !== id);
|
||||
return;
|
||||
} else {
|
||||
alert('Error deleting person');
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<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="divider"></div>
|
||||
</div>
|
||||
<ul class="list bg-base-100 rounded-box shadow-md">
|
||||
<li class="p-4 pb-2 text-xs tracking-wide opacity-60">{managed_profiles()}</li>
|
||||
{#each managed_profiles_list as profile}
|
||||
<li class="list-row">
|
||||
<div>
|
||||
<div>{profile.first_name + ' ' + profile.last_name}</div>
|
||||
<div class="text-xs font-semibold uppercase opacity-60">{profile.id}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs font-semibold uppercase opacity-60">
|
||||
{admin() + ' ' + from_time().toLowerCase() + ': ' + profile.adminSince}
|
||||
</div>
|
||||
<div class="text-xs font-semibold uppercase opacity-60">{profile.label![0]}</div>
|
||||
</div>
|
||||
<button
|
||||
class="btn btn-success btn-soft"
|
||||
onclick={() => {
|
||||
addRelationship(profile.id!);
|
||||
}}>
|
||||
{add_relationship()}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-success btn-soft"
|
||||
onclick={() => {
|
||||
createRelationshipAndProfile(profile.id!);
|
||||
}}>
|
||||
{create_relationship_and_person()}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-secondary"
|
||||
onclick={() => {
|
||||
editProfile(profile.id!);
|
||||
}}>
|
||||
{edit()}
|
||||
</button>
|
||||
{#if profile.label?.includes('DeletedPerson')}
|
||||
<button
|
||||
class="btn btn-error"
|
||||
onclick={() => {
|
||||
hardDeleteProfile(profile.id!);
|
||||
}}
|
||||
>
|
||||
{hard_delete()}
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
class="btn btn-error"
|
||||
onclick={() => {
|
||||
deleteProfile(profile.id!);
|
||||
}}
|
||||
>
|
||||
{delete_profile()}
|
||||
</button>
|
||||
{/if}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
27
apps/app/src/lib/admin/ModalButtons.svelte
Normal file
27
apps/app/src/lib/admin/ModalButtons.svelte
Normal file
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
add_relationship,
|
||||
back,
|
||||
biography,
|
||||
close,
|
||||
create,
|
||||
create_person,
|
||||
edit,
|
||||
relation,
|
||||
save
|
||||
} from '$lib/paraglide/messages';
|
||||
export let createProfile: () => void;
|
||||
export let onClose: () => void;
|
||||
</script>
|
||||
|
||||
<div class="flex items-center justify-between p-2">
|
||||
<h3 class="text-lg font-bold">{relation()}</h3>
|
||||
<div class="space-x-2">
|
||||
<button class="btn btn-success btn-sm" on:click={createProfile}>
|
||||
{'+ ' + create_person()}
|
||||
</button>
|
||||
<button class="btn btn-error btn-sm" on:click={onClose}>
|
||||
{close()}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
@@ -532,14 +532,10 @@ export interface components {
|
||||
};
|
||||
Admin: {
|
||||
id?: number;
|
||||
label?: string[];
|
||||
first_name?: string;
|
||||
adminSince?: number;
|
||||
last_name?: string;
|
||||
EndId?: number;
|
||||
EndElementId?: string;
|
||||
Props?: {
|
||||
added?: number;
|
||||
};
|
||||
};
|
||||
AdminRelationship: {
|
||||
Id?: number;
|
||||
|
91
apps/app/src/lib/relationship/EdgeMenu.svelte
Normal file
91
apps/app/src/lib/relationship/EdgeMenu.svelte
Normal file
@@ -0,0 +1,91 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import {
|
||||
add_relationship,
|
||||
remove,
|
||||
create_relationship_and_person,
|
||||
add_administrator
|
||||
} from '$lib/paraglide/messages';
|
||||
import type { Edge } from '@xyflow/svelte';
|
||||
|
||||
export let edge: Edge;
|
||||
export let XUserId: string;
|
||||
export let top: number | undefined;
|
||||
export let left: number | undefined;
|
||||
export let right: number | undefined;
|
||||
export let bottom: number | undefined;
|
||||
export let onClick: () => void;
|
||||
export let deleteEdge: () => void;
|
||||
|
||||
let contextMenu: HTMLDivElement;
|
||||
let isAdmin: boolean = false;
|
||||
onMount(() => {
|
||||
if (top) {
|
||||
contextMenu.style.top = `${top}px`;
|
||||
}
|
||||
if (left) {
|
||||
contextMenu.style.left = `${left}px`;
|
||||
}
|
||||
if (right) {
|
||||
contextMenu.style.right = `${right}px`;
|
||||
}
|
||||
if (bottom) {
|
||||
contextMenu.style.bottom = `${bottom}px`;
|
||||
}
|
||||
|
||||
fetch(`/api/admin/${edge.source}/${XUserId}`)
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
isAdmin = true;
|
||||
} else {
|
||||
isAdmin = false;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error fetching admin status:', error);
|
||||
});
|
||||
fetch(`/api/admin/${edge.target}/${XUserId}`)
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
isAdmin = true;
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error fetching admin status:', error);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
role="menu"
|
||||
tabindex="-1"
|
||||
bind:this={contextMenu}
|
||||
class="context-menu bg-primary-100 rounded-lg shadow-lg"
|
||||
onclick={onClick}
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Esc' || e.key === ' ' || e.key === 'Escape') {
|
||||
onClick();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if isAdmin}
|
||||
<button onclick={deleteEdge} class="btn">{remove()}</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.context-menu {
|
||||
border-style: solid;
|
||||
box-shadow: 10px 19px 20px rgba(0, 0, 0, 10%);
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.context-menu button {
|
||||
border: none;
|
||||
display: block;
|
||||
padding: 0.5em;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
@@ -1,5 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { child, date, description, edit, file, from_time, media_title, notes, parent, relation_type, sibling, spouse, title, until, upload } from '$lib/paraglide/messages';
|
||||
import {
|
||||
child,
|
||||
from_time,
|
||||
id,
|
||||
notes,
|
||||
parent,
|
||||
relation,
|
||||
relation_type,
|
||||
sibling,
|
||||
spouse,
|
||||
until,
|
||||
} from '$lib/paraglide/messages';
|
||||
import type { Edge } from '@xyflow/svelte';
|
||||
import ModalButtons from '$lib/relationship/ModalButtons.svelte';
|
||||
import type { components, operations } from '$lib/api/api.gen';
|
||||
@@ -29,9 +40,8 @@
|
||||
});
|
||||
let relationshiptype: 'sibling' | 'child' | 'parent' | 'spouse' | undefined = $state('sibling');
|
||||
|
||||
async function getRelationships(startId: string, endId :string) {
|
||||
if (startId === undefined || endId === undefined) {
|
||||
alert('');
|
||||
async function getRelationships(startId: string, endId: string) {
|
||||
if (startId === undefined || endId === undefined || startId === '' || endId === '' || startId === endId) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -50,11 +60,21 @@
|
||||
relationships.push((await response.json()) as components['schemas']['dbtypeRelationship']);
|
||||
}
|
||||
|
||||
getRelationships(startNode,endNode);
|
||||
getRelationships(startNode,endNode);
|
||||
if (!createRelationship){
|
||||
getRelationships(startNode, endNode);
|
||||
getRelationships(endNode, startNode);
|
||||
}
|
||||
|
||||
async function save() {
|
||||
for (const r of relationships) {
|
||||
if (!r.Props) {
|
||||
console.log('No properties found for relationship', r);
|
||||
continue;
|
||||
}
|
||||
if (r.Props.verified === undefined) {
|
||||
r.Props.verified = false;
|
||||
}
|
||||
console.log('Saving relationship', r.StartId, r.EndId, r.Props);
|
||||
const patchBody: components['schemas']['FamilyRelationship'] = r.Props ?? {};
|
||||
|
||||
const response = await fetch(`/api/relationship/${r.StartId}/${r.EndId}`, {
|
||||
@@ -66,6 +86,13 @@
|
||||
if (!response.ok) {
|
||||
console.log(`Failed to save relationship ${r.StartId} → ${r.EndId}`);
|
||||
}
|
||||
|
||||
if (response.status === 200) {
|
||||
console.log(`Relationship ${r.StartId} → ${r.EndId} saved successfully`);
|
||||
} else {
|
||||
console.log(`Failed to save relationship ${r.StartId} → ${r.EndId}`);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
closeModal();
|
||||
@@ -85,12 +112,13 @@
|
||||
}
|
||||
|
||||
let body: operations['createRelationship']['requestBody']['content']['application/json'] = {
|
||||
id1: startNode,
|
||||
id2: endNode,
|
||||
id1: Number(startNode),
|
||||
id2: Number(endNode),
|
||||
type: relationshiptype,
|
||||
relationship: newRelationship
|
||||
};
|
||||
|
||||
console.log('Creating relationship', body);
|
||||
const response = await fetch(`/api/relationship`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -98,11 +126,11 @@
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
console.log('Cannot create relationship');
|
||||
console.log('Cannot create relationship'+', status: ' + response.status);
|
||||
return;
|
||||
}
|
||||
|
||||
const created = await response.json() as components['schemas']['dbtypeRelationship'][];
|
||||
const created = (await response.json()) as components['schemas']['dbtypeRelationship'][];
|
||||
relationships.push(...created);
|
||||
|
||||
let newEdges: Edge[] = [];
|
||||
@@ -112,19 +140,19 @@
|
||||
source: r.StartElementId!,
|
||||
target: r.EndElementId!,
|
||||
type: 'relationship',
|
||||
data: {...r.Props, type: r.Type},
|
||||
data: { ...r.Props, type: r.Type }
|
||||
});
|
||||
}
|
||||
onCreation(newEdges);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<div class="modal modal-open z-8">
|
||||
<div class="modal-box w-full max-w-xl">
|
||||
<div class="modal-box w-full max-w-xl gap-4">
|
||||
<div class="bg-base-100 sticky top-0 z-7">
|
||||
<ModalButtons
|
||||
{editorMode}
|
||||
createMode={createRelationship}
|
||||
onCreate={createNewRelationship}
|
||||
onClose={closeModal}
|
||||
onSave={save}
|
||||
@@ -145,72 +173,69 @@
|
||||
<option value="spouse">{spouse()}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-control mt-1">
|
||||
<p><strong>{id().toLowerCase()}:</strong>{startNode}</p>
|
||||
</div>
|
||||
<div class="form-control mt-1">
|
||||
<label for="endNode" class="label">{relation()+' '+id().toLowerCase()}:</label>
|
||||
<input id="endNode" type="text" bind:value={endNode} class="input input-bordered w-full"/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if !createRelationship}
|
||||
<!-- Editor mode: show all existing relationships -->
|
||||
{#each relationships as r, index}
|
||||
<div class="border-base-300 mt-4 rounded border p-4">
|
||||
<div class="form-control">
|
||||
{#if editorMode}
|
||||
<label for={`relationshiptype-${index}`} class="label">{relation_type()}</label>
|
||||
<select id={`relationshiptype-${index}`} bind:value={r.Type} class="select select-bordered">
|
||||
<option value="sibling">{sibling()}</option>
|
||||
<option value="child">{child()}</option>
|
||||
<option value="parent">{parent()}</option>
|
||||
<option value="spouse">{spouse()}</option>
|
||||
</select>
|
||||
{:else}
|
||||
<p><strong>{relation_type()}:</strong> {r.Type}</p>
|
||||
{/if}
|
||||
<p><strong>{relation_type()}:</strong> {r.Type}</p>
|
||||
</div>
|
||||
<div class="form-control mt-2">
|
||||
{#if editorMode}
|
||||
<label for={`verified-${index}`} class="label">Verified</label>
|
||||
<input
|
||||
id={`verified-${index}`}
|
||||
type="checkbox"
|
||||
bind:value={r.Props!.verified}
|
||||
class="checkbox"
|
||||
/>
|
||||
<label for={`verified-${index}`} class="label">Verified</label>
|
||||
<input
|
||||
id={`verified-${index}`}
|
||||
type="checkbox"
|
||||
bind:checked={r.Props!.verified}
|
||||
class="checkbox"
|
||||
/>
|
||||
{:else}
|
||||
<p><strong>Verified:</strong>{r.Props?.verified}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="form-control mt-2">ú
|
||||
<div class="form-control mt-2">
|
||||
{#if editorMode}
|
||||
<label for={`notes-${index}`} class="label">{notes()}</label>
|
||||
<textarea
|
||||
id={`notes-${index}`}
|
||||
bind:value={r.Props!.notes}
|
||||
class="textarea textarea-bordered w-full"
|
||||
>
|
||||
</textarea>
|
||||
<label for={`notes-${index}`} class="label">{notes()}</label>
|
||||
<textarea
|
||||
id={`notes-${index}`}
|
||||
bind:value={r.Props!.notes}
|
||||
class="textarea textarea-bordered w-full"
|
||||
>
|
||||
</textarea>
|
||||
{:else}
|
||||
<p><strong>{notes()}:</strong> {r.Props?.notes}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="form-control mt-2">
|
||||
{#if editorMode}
|
||||
<label for={`from-${index}`} class="label">{from_time()}</label>
|
||||
<input
|
||||
id={`from-${index}`}
|
||||
type="date"
|
||||
bind:value={r.Props!.from}
|
||||
class="input input-bordered w-full"
|
||||
/>
|
||||
<label for={`from-${index}`} class="label">{from_time()}</label>
|
||||
<input
|
||||
id={`from-${index}`}
|
||||
type="date"
|
||||
bind:value={r.Props!.from}
|
||||
class="input input-bordered w-full"
|
||||
/>
|
||||
{:else}
|
||||
<p><strong>{from_time()}:</strong> {r.Props?.from}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="form-control mt-2">
|
||||
{#if editorMode}
|
||||
<label for={`to-${index}`} class="label">{until()}</label>
|
||||
<input
|
||||
id={`to-${index}`}
|
||||
type="date"
|
||||
bind:value={r.Props!.to}
|
||||
class="input input-bordered w-full"
|
||||
/>
|
||||
<label for={`to-${index}`} class="label">{until()}</label>
|
||||
<input
|
||||
id={`to-${index}`}
|
||||
type="date"
|
||||
bind:value={r.Props!.to}
|
||||
class="input input-bordered w-full"
|
||||
/>
|
||||
{:else}
|
||||
<p><strong>{until()}:</strong> {r.Props?.to}</p>
|
||||
{/if}
|
||||
|
@@ -1,5 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { add_relationship, back, biography, close, create, edit, save } from '$lib/paraglide/messages';
|
||||
import {
|
||||
add_relationship,
|
||||
back,
|
||||
biography,
|
||||
close,
|
||||
create,
|
||||
edit,
|
||||
relation,
|
||||
save
|
||||
} from '$lib/paraglide/messages';
|
||||
|
||||
export let editorMode = false;
|
||||
export let createMode = false;
|
||||
@@ -11,23 +20,21 @@
|
||||
</script>
|
||||
|
||||
<div class="flex items-center justify-between p-2">
|
||||
<h3 class="text-lg font-bold">{biography()}</h3>
|
||||
<h3 class="text-lg font-bold">{relation()}</h3>
|
||||
<div class="space-x-2">
|
||||
{#if !createMode}
|
||||
<button class="btn btn-secondary btn-sm" on:click={onToggleEdit}>
|
||||
{editorMode ? back() : edit()}
|
||||
</button>
|
||||
{/if}
|
||||
{#if editorMode}
|
||||
{#if createMode}
|
||||
<button class="btn btn-accent btn-sm" on:click={onCreate}>
|
||||
{add_relationship()}
|
||||
</button>
|
||||
{:else}
|
||||
<button class="btn btn-accent btn-sm" on:click={onSave}>
|
||||
{save()}
|
||||
</button>
|
||||
{/if}
|
||||
{#if createMode}
|
||||
<button class="btn btn-accent btn-sm" on:click={onCreate}>
|
||||
{add_relationship()}
|
||||
</button>
|
||||
{:else if editorMode}
|
||||
<button class="btn btn-accent btn-sm" on:click={onSave}>
|
||||
{save()}
|
||||
</button>
|
||||
{/if}
|
||||
<button class="btn btn-error btn-sm" on:click={onClose}>
|
||||
{close()}
|
||||
|
12
apps/app/src/lib/relationship/model.ts
Normal file
12
apps/app/src/lib/relationship/model.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { Edge } from '@xyflow/svelte';
|
||||
|
||||
export interface RelationshipMenu {
|
||||
edge: Edge;
|
||||
XUserId: string;
|
||||
top: number | undefined;
|
||||
left: number | undefined;
|
||||
right: number | undefined;
|
||||
bottom: number | undefined;
|
||||
onClick: () => void;
|
||||
deleteEdge: () => void;
|
||||
}
|
@@ -1,8 +1,11 @@
|
||||
<script lang="ts">
|
||||
import { edit } from './../lib/paraglide/messages/en.js';
|
||||
import CreateRelationship from '$lib/relationship/Modal.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
import { nodeTypes, edgeTypes } from '$lib/graph/model';
|
||||
import { title, family_tree } from '$lib/paraglide/messages.js';
|
||||
import { title, family_tree, select } from '$lib/paraglide/messages.js';
|
||||
import type { RelationshipMenu } from '$lib/relationship/model.ts';
|
||||
import AdminMenu from '$lib/admin/Modal.svelte';
|
||||
|
||||
import { SvelteFlowProvider, SvelteFlow, Controls, MiniMap } from '@xyflow/svelte';
|
||||
import '@xyflow/svelte/dist/style.css';
|
||||
@@ -33,6 +36,7 @@
|
||||
let openPersonMenu: NodeMenu | undefined = $state(undefined);
|
||||
let with_out_spouse = $state(false);
|
||||
let createRelationship = $state(false);
|
||||
let adminMenu = $state(false);
|
||||
|
||||
let familyTreeDAG = new FamilyTree();
|
||||
let layout = familyTreeDAG.getLayoutedElements(
|
||||
@@ -46,6 +50,7 @@
|
||||
let edges = $state.raw<Edge[]>([] as Edge[]);
|
||||
|
||||
let relationshipStart: number | null = $state(null);
|
||||
let relationshipMenu = $state(undefined as RelationshipMenu | undefined);
|
||||
let createPerson = $state(false);
|
||||
|
||||
let clientWidth: number | undefined = $state();
|
||||
@@ -104,6 +109,12 @@
|
||||
},
|
||||
addRelationship: () => {
|
||||
relationshipStart = Number(node.data.id);
|
||||
createRelationship = true;
|
||||
selectedRelationship = {
|
||||
id: 'relationship' + node.data.id,
|
||||
source: String(relationshipStart),
|
||||
target: String(node.data.id)
|
||||
};
|
||||
openPersonMenu = undefined;
|
||||
},
|
||||
addAdmin: () => {
|
||||
@@ -181,7 +192,7 @@
|
||||
};
|
||||
|
||||
const handleConnectEnd: OnConnectEnd = (event, connectionState) => {
|
||||
|
||||
event.preventDefault();
|
||||
const sourceNodeId = connectionState.fromNode?.data.id;
|
||||
if (sourceNodeId === undefined) return;
|
||||
relationshipStart = Number(sourceNodeId);
|
||||
@@ -190,7 +201,7 @@
|
||||
selectedRelationship = {
|
||||
id: 'relationship' + connectionState.toNode?.data.id,
|
||||
source: String(relationshipStart),
|
||||
target: String(connectionState.toNode?.data.id),
|
||||
target: String(connectionState.toNode?.data.id)
|
||||
};
|
||||
return;
|
||||
}
|
||||
@@ -212,20 +223,42 @@
|
||||
bind:nodes
|
||||
bind:edges
|
||||
onconnectend={handleConnectEnd}
|
||||
onedgeclick={({ edge, event }: {
|
||||
edge: Edge;
|
||||
event: MouseEvent;
|
||||
})=> {
|
||||
onedgeclick={({ edge, event }: { edge: Edge; event: MouseEvent }) => {
|
||||
selectedRelationship = edge;
|
||||
selectedRelationship.source = String(edge.source.replace('person', ''));
|
||||
selectedRelationship.target = String(edge.target.replace('person', ''));
|
||||
}}
|
||||
onnodeclick={handleNodeClickFunc}
|
||||
onnodecontextmenu={handleContextMenu}
|
||||
onedgecontextmenu={({ edge, event }: { edge: Edge; event: MouseEvent }) => {
|
||||
selectedRelationship = edge;
|
||||
selectedRelationship.source = String(edge.source.replace('person', ''));
|
||||
selectedRelationship.target = String(edge.target.replace('person', ''));
|
||||
if (clientHeight === undefined || clientWidth === undefined) {
|
||||
clientHeight = window.innerHeight;
|
||||
clientWidth = window.innerWidth;
|
||||
}
|
||||
relationshipMenu = {
|
||||
XUserId: data.id,
|
||||
edge: selectedRelationship,
|
||||
onClick: () => {
|
||||
relationshipMenu = undefined;
|
||||
},
|
||||
deleteEdge: () => {
|
||||
edges = edges.filter((e) => e.id !== edge.id);
|
||||
relationshipMenu = undefined;
|
||||
},
|
||||
top: event.clientY < clientHeight - 200 ? event.clientY : undefined,
|
||||
left: event.clientX < clientWidth - 200 ? event.clientX : undefined,
|
||||
right: event.clientX >= clientWidth - 200 ? clientWidth - event.clientX : undefined,
|
||||
bottom: event.clientY >= clientHeight - 200 ? clientHeight - event.clientY : undefined
|
||||
};
|
||||
}}
|
||||
onpaneclick={handlePaneClick}
|
||||
class="!bg-base-200"
|
||||
{nodeTypes}
|
||||
{edgeTypes}
|
||||
fitView
|
||||
onlyRenderVisibleElements={false}
|
||||
fitView={true}
|
||||
>
|
||||
<MiniMap class="!bg-base-300" />
|
||||
<Controls class="!bg-base-300" />
|
||||
@@ -260,18 +293,55 @@
|
||||
createRelationship = false;
|
||||
selectedRelationship = undefined;
|
||||
relationshipStart = null;
|
||||
layout = familyTreeDAG.getLayoutedElements(
|
||||
nodes,
|
||||
edges,
|
||||
tailwindClassToPixels('w-40') || 160,
|
||||
tailwindClassToPixels('h-40') || 160,
|
||||
'TB'
|
||||
);
|
||||
edges = [...layout.Edges];
|
||||
nodes = [...layout.Nodes];
|
||||
}}
|
||||
startNode={String(relationshipStart)}
|
||||
startNode={String(selectedRelationship.source)}
|
||||
endNode={String(selectedRelationship.target)}
|
||||
/>
|
||||
{/if}
|
||||
{#if openPersonMenu !== undefined}
|
||||
<PersonMenu {...openPersonMenu!} />
|
||||
{/if}
|
||||
{#if adminMenu}
|
||||
<AdminMenu
|
||||
createProfile={() => {
|
||||
createPerson = true;
|
||||
relationshipStart = null;
|
||||
}}
|
||||
createRelationshipAndProfile={(id: number) => {
|
||||
createPerson = true;
|
||||
relationshipStart = id;
|
||||
}}
|
||||
addRelationship={(id: number) => {
|
||||
createRelationship = true;
|
||||
selectedRelationship = {
|
||||
id: 'relationship' + id,
|
||||
source: String(id),
|
||||
target: String(id)
|
||||
};
|
||||
}}
|
||||
closeModal={() => {
|
||||
adminMenu = false;
|
||||
}}
|
||||
editProfile={(id: number) => {
|
||||
openPersonPanel = true;
|
||||
selectedPerson = { id: String(id) };
|
||||
}}
|
||||
onChange={() => {}}
|
||||
/>
|
||||
{/if}
|
||||
</SvelteFlow>
|
||||
</SvelteFlowProvider>
|
||||
</div>
|
||||
|
||||
<div class="absolute top-2 left-2 flex flex-row items-center gap-2">
|
||||
<HamburgerIcon />
|
||||
<HamburgerIcon open_admin_panel={()=>{adminMenu=!adminMenu}}/>
|
||||
</div>
|
||||
|
25
apps/app/src/routes/api/managed_profiles/+server.ts
Normal file
25
apps/app/src/routes/api/managed_profiles/+server.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { client } from '$lib/api/client';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import type { RequestEvent } from './$types';
|
||||
|
||||
export async function GET(event: RequestEvent): Promise<Response> {
|
||||
if (event.locals.session === null) {
|
||||
return redirect(302, '/login');
|
||||
}
|
||||
|
||||
const response = await client.GET('/managed_profiles', {
|
||||
params: {
|
||||
header: { 'X-User-ID': event.locals.session.userId }
|
||||
}
|
||||
});
|
||||
|
||||
if (response.response.ok) {
|
||||
return new Response(null, {
|
||||
status: response.response.status
|
||||
});
|
||||
} else {
|
||||
return new Response(JSON.stringify(response.error), {
|
||||
status: response.response.status
|
||||
});
|
||||
}
|
||||
}
|
@@ -1,3 +1,12 @@
|
||||
MATCH (a)-[r1:Admin]->(b)
|
||||
WHERE id(a) = $id
|
||||
RETURN collect({id: id(b), first_name: b.first_name, last_name: b.last_name, adminSince: r1.added}) as managed;
|
||||
RETURN
|
||||
collect(
|
||||
{
|
||||
id: id(b),
|
||||
label: labels(b),
|
||||
first_name: b.first_name,
|
||||
last_name: b.last_name,
|
||||
adminSince: r1.added
|
||||
}
|
||||
) AS managed;
|
@@ -58,15 +58,11 @@ const (
|
||||
|
||||
// Admin defines model for Admin.
|
||||
type Admin struct {
|
||||
EndElementId *string `json:"EndElementId,omitempty"`
|
||||
EndId *int `json:"EndId,omitempty"`
|
||||
Props *struct {
|
||||
Added *int `json:"added,omitempty"`
|
||||
} `json:"Props,omitempty"`
|
||||
AdminSince *int `json:"adminSince,omitempty"`
|
||||
FirstName *string `json:"first_name,omitempty"`
|
||||
Id *int `json:"id,omitempty"`
|
||||
LastName *string `json:"last_name,omitempty"`
|
||||
AdminSince *int `json:"adminSince,omitempty"`
|
||||
FirstName *string `json:"first_name,omitempty"`
|
||||
Id *int `json:"id,omitempty"`
|
||||
Label *[]string `json:"label,omitempty"`
|
||||
LastName *string `json:"last_name,omitempty"`
|
||||
}
|
||||
|
||||
// AdminRelationship defines model for AdminRelationship.
|
||||
|
Reference in New Issue
Block a user