f047_neu_Edit-der-Namen #28

Merged
trachi93 merged 14 commits from f047_neu_Edit-der-Namen into development 2025-08-19 09:30:13 +02:00
4 changed files with 456 additions and 228 deletions
Showing only changes of commit a91d7292c5 - Show all commits

View File

@@ -0,0 +1,93 @@
<script lang="ts">
import Edit from '$lib/icons/Edit.svelte';
import Trash from '$lib/icons/Trash.svelte';
import { tick } from 'svelte';
interface ListItem {
name: string;
token?: string;
// add other properties as needed
}
let {
list,
editedName = $bindable(),
currentName,
onSave = () => {},
onDelete = () => {}
} = $props();
let names = list.map((l: ListItem) => l.name);
let localName = $state(currentName);
let wasCancelled = $state(false);
let error: string = $derived(validateName(localName));
let manualError = $state('');
let isEditing = $state(false);
let inputRef: HTMLInputElement;
function validateName(name: string) {
const trimmed = name.trim();
if (!trimmed) {
return 'Name darf nicht leer sein.';
}
const duplicate = list.some(
(item: ListItem) => item.name === trimmed && item.name !== currentName
);
if (duplicate) return 'Name existiert bereits.';
return '';
}
function commitIfValid() {
if (!error && !wasCancelled && localName != currentName) {
editedName = localName.trim();
onSave(editedName, currentName);
} else {
localName = currentName;
resetEdit();
}
}
function resetEdit() {
wasCancelled = false;
manualError = '';
inputRef?.blur();
isEditing = false;
}
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Enter') {
event.preventDefault();
commitIfValid();
} else if (event.key === 'Escape') {
event.preventDefault();
localName = currentName;
resetEdit();
}
}
async function startEdit() {
isEditing = true;
await tick();
inputRef?.focus();
}
</script>
<div>
<input
bind:this={inputRef}
bind:value={localName}
onblur={commitIfValid}
onkeydown={handleKeydown}
/>
<button onclick={startEdit}><Edit /></button>
<button onclick={() => onDelete(currentName)}><Trash /></button>
{#if manualError || error}
<p style="color: red;">{manualError || error}</p>
{/if}
</div>

View File

@@ -1,17 +1,28 @@
import { getVorgangByToken, getCrimesListByToken } from '$lib/server/vorgangService';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params, url }) => {
export const load: PageServerLoad = async ({fetch, params, url }) => {
const vorgangToken = params.vorgang;
const vorgangPIN = url.searchParams.get('pin');
const adminRes = await fetch(`/api/user`)
const user = await adminRes.json()
const crimesList = await getCrimesListByToken(vorgangToken); // TatortList zum Vorgang
const vorgang = getVorgangByToken(vorgangToken); //einzelner Vorgang
const vorgangObjekt = getVorgangByToken(vorgangToken); //einzelner Vorgang //TypeScript darf nicht undefined sein
let vorgangName:string;
if(vorgangObjekt){
vorgangName = vorgangObjekt.name;
}else{
vorgangName = '';
}
return {
crimesList,
vorgangPIN,
vorgang
crimesList, //crimesList
vorgangPIN, //caseToken
vorgangName, //caseId?
vorgangObjekt,
user
};
};

View File

@@ -1,7 +1,5 @@
<script lang="ts">
Review

Wie du beschrieben hast, unnötige Kommentare und console.logs raus

Wie du beschrieben hast, unnötige Kommentare und console.logs raus
Review

Sollte soweit passen, ansonsten bitte selbstständig rauslöschen

Sollte soweit passen, ansonsten bitte selbstständig rauslöschen
import shortenFileSize from '$lib/helper/shortenFileSize';
import { page } from '$app/stores';
import timeElapsed from '$lib/helper/timeElapsed';
import Alert from '$lib/components/Alert.svelte';
@@ -11,14 +9,16 @@
import ModalContent from '$lib/components/Modal/ModalContent.svelte';
import ModalFooter from '$lib/components/Modal/ModalFooter.svelte';
import Cube from '$lib/icons/Cube.svelte';
import Edit from '$lib/icons/Edit.svelte';
import Trash from '$lib/icons/Trash.svelte';
/** export let data; */
/** @type {import('./$types').PageData} */
export let data;
import EditableItem from '$lib/components/EditableItem.svelte';
console.log(data);
import { invalidate } from '$app/navigation';
import { page } from '$app/stores';
//Seite für die Tatort-Liste
let { data } = $props();
console.log('tatorte: debug ', data);
interface ListItem {
name: string;
@@ -28,264 +28,378 @@
// add other properties as needed
}
const vorgang = data.vorgang;
const crimesList: ListItem[] = data.crimesList;
const vorgangPIN: string = data.vorgangPIN;
let open = false;
$: open;
let inProgress = false;
$: inProgress;
let err = false;
$: err;
let rename_input;
$: rename_input;
let vorgang: string = data.vorgangName; // let vorgang = data.caseId; <!--caseId, vorgang.name aber das funktioniert nicht richtig, daher in server.ts geändert-->
let crimesList: ListItem[] = $state(data.crimesList);
const vorgangPIN: string = data.vorgangPIN; //caseToken?? // const token: string | null = data.caseToken;
//Variablen für Modal
let open = $state(false);
let inProgress = $state(false);
let err = $state(false);
function uploadSuccessful() {
open = false;
}
function defocus_element(i: number) {
let item = crimesList[i];
let text_field_id = `label__${item.name}`;
//Variabeln für EditableItem
let names: string[] = $state(crimesList.map((l) => l.name));
let editedName: string = $state('');
let currentName: string = $state('');
let text_field = document.getElementById(text_field_id);
if (text_field) {
text_field.setAttribute('contenteditable', 'false');
text_field.textContent = item.name;
}
let editingId: number;
// reshow button
crimesList[i].show_button = true;
return;
}
// let admin = data?.user?.admin;
let admin = true;
async function handleEditFieldInput(ev: KeyboardEvent, listItemIndex: number) {
let item = crimesList[listItemIndex];
if (ev.key == 'Escape') {
let text_field_id = `label__${item.name}`;
let rename_input;
let text_field = document.getElementById(text_field_id);
if (text_field) {
text_field.setAttribute('contenteditable', 'false');
text_field.textContent = item.name;
}
$effect(() => {
console.log('rename_input hat sich geändert:', rename_input);
});
// reshow button
item.show_button = true;
return;
}
if (ev.key == 'Enter') {
let name_field = ev.currentTarget as HTMLElement | null;
let new_name = name_field
? name_field.textContent || (name_field as any).innerText || ''
: '';
async function handleSave(newName: string, oldName: string) {
console.log('Eltern, speichern erfolgreich', newName, oldName);
try {
const res = await fetch(`/api/list/${vorgang}/${oldName}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ vorgang, oldName, newName })
});
const clone = res.clone();
const data = await res.json();
let message = data.message;
let error = !data.success;
if (new_name == '') {
alert('Bitte einen gültigen Namen eingeben.');
ev.preventDefault();
return;
}
console.log('Tatort Update: ', message);
// actual upload
// -------------
// to prevent from item being selected
ev.preventDefault();
// construct PUT URL
const url = $page.url.pathname.split('?')[0];
let data_obj: { new_name: string; old_name: string } = { new_name: '', old_name: '' };
data_obj['new_name'] = new_name;
data_obj['old_name'] =
ev.currentTarget && (ev.currentTarget as HTMLElement).id
? (ev.currentTarget as HTMLElement).id.split('__')[1]
: '';
open = true;
inProgress = true;
const response = await fetch(url, { method: 'PUT', body: JSON.stringify(data_obj) });
inProgress = false;
if (!response.ok) {
err = true;
if (response.status == 400) {
let json_res = await response.json();
return;
}
throw new Error(`Fehlgeschlagen: ${response.status}`);
if (!res.ok) {
const msg = await clone.text();
console.error('❌ Fehler beim Speichern:', msg);
} else {
uploadSuccessful();
setTimeout(() => {
window.location.reload();
}, 500);
console.log('✅ Erfolgreich gespeichert:', newName);
await invalidate('');
currentName = newName;
}
// --- upload finished ---
return;
} catch (err) {
console.error('⚠️ Netzwerkfehler:', err);
}
}
function constructMailToLink() {
const subject = 'Link zum Tatvorgang';
const link = $page.url.toString().split('?')[0];
const body = `Hallo,
async function handleDelete(tatort: string) {
let url = new URL($page.url);
url.pathname += `/${tatort}`;
console.log('Delete tatort: ', `/api${url.pathname}`, url.pathname);
hier ist der Link zum Tatvorgang:
${link}
try {
const res = await fetch(`/api${url.pathname}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ vorgang, tatort })
});
Der Zugangs-PIN wird zur Sicherheit über einen zweiten Kommunikationskanal übermittelt.
Mit freundlichen Grüßen,
`;
const mailtoLink = `mailto:?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
return mailtoLink;
if (!res.ok) {
const msg = await res.text();
console.error('❌ Fehler beim Löschen:', msg);
} else {
console.log('🗑️ Erfolgreich gelöscht:', url.pathname);
}
} catch (err) {
console.error('⚠️ Netzwerkfehler beim Löschen:', err);
}
}
// function defocus_element(i: number) {
// let item = crimesList[i];
// let text_field_id = `label__${item.name}`;
// let text_field = document.getElementById(text_field_id);
// if (text_field) {
// text_field.setAttribute('contenteditable', 'false');
// text_field.textContent = item.name;
// }
// // reshow button
// crimesList[i].show_button = true;
// return;
// }
// async function handleEditFieldInput(ev: KeyboardEvent, listItemIndex: number) {
// let item = crimesList[listItemIndex];
// if (ev.key == 'Escape') {
// let text_field_id = `label__${item.name}`;
// let text_field = document.getElementById(text_field_id);
// if (text_field) {
// text_field.setAttribute('contenteditable', 'false');
// text_field.textContent = item.name;
// }
// // reshow button
// item.show_button = true;
// return;
// }
// if (ev.key == 'Enter') {
// let name_field = ev.currentTarget as HTMLElement | null;
// let new_name = name_field
// ? name_field.textContent || (name_field as any).innerText || ''
// : '';
// if (new_name == '') {
// alert('Bitte einen gültigen Namen eingeben.');
// ev.preventDefault();
// return;
// }
// // actual upload
// // -------------
// // to prevent from item being selected
// ev.preventDefault();
// // construct PUT URL
// const url = $page.url.pathname.split('?')[0];
// let data_obj: { new_name: string; old_name: string } = { new_name: '', old_name: '' };
// data_obj['new_name'] = new_name;
// data_obj['old_name'] =
// ev.currentTarget && (ev.currentTarget as HTMLElement).id
// ? (ev.currentTarget as HTMLElement).id.split('__')[1]
// : '';
// open = true;
// inProgress = true;
// const response = await fetch(url, { method: 'PUT', body: JSON.stringify(data_obj) });
// inProgress = false;
// if (!response.ok) {
// err = true;
// if (response.status == 400) {
// let json_res = await response.json();
// return;
// }
// throw new Error(`Fehlgeschlagen: ${response.status}`);
// } else {
// uploadSuccessful();
// setTimeout(() => {
// window.location.reload();
// }, 500);
// }
// // --- upload finished ---
// return;
// }
// }
// function constructMailToLink() {
// const subject = 'Link zum Tatvorgang';
// const link = $page.url.toString().split('?')[0];
// const body = `Hallo,
// hier ist der Link zum Tatvorgang:
// ${link}
// Der Zugangs-PIN wird zur Sicherheit über einen zweiten Kommunikationskanal übermittelt.
// Mit freundlichen Grüßen,
// `;
// const mailtoLink = `mailto:?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
// return mailtoLink;
// }
</script>
<div class="-z-10 bg-white">
<div class="flex flex-col items-center justify-center w-full">
<h1 class="text-xl">Vorgang {vorgang.name}</h1>
{#if data?.user?.admin}
Zugangs-PIN: {vorgang.pin}
<a href={constructMailToLink()}><Button>Share Link</Button></a>
{/if}
</div>
<div class="mx-auto flex justify-center max-w-7xl h-full">
<ul class="divide-y divide-gray-100">
{#each crimesList as item, crimeListItemIndex}
<li>
<a
href="/view/{$page.params.vorgang}/{item.name}?pin={vorgangPIN}"
class=" flex justify-between gap-x-6 py-5"
aria-label="zum 3D-modell"
>
<div class=" flex gap-x-4">
<Cube />
<div class="min-w-0 flex-auto">
{#if data?.user?.admin}
<span
id="label__{item.name}"
class="text-sm font-semibold leading-6 text-gray-900 inline-block min-w-1"
contenteditable={!item.show_button}
role="textbox"
tabindex="0"
aria-label="Dateiname bearbeiten"
on:focusout={() => {
defocus_element(crimeListItemIndex);
}}
on:keydown|stopPropagation={// event needed to identify ID
// TO-DO: check if event is needed or if index is sufficient
async (ev) => {
handleEditFieldInput(ev, crimeListItemIndex);
}}>{item.name}</span
>
{#if vorgang}
<div class="-z-10 bg-white">
<div class="flex flex-col items-center justify-center w-full">
<h1 class="text-xl">Vorgang {vorgang}</h1>
{#if item.show_button}
<!-- {#if data?.user?.admin}
Zugangs-PIN: {vorgang.pin}
<a href={constructMailToLink()}><Button>Share Link</Button></a>
{/if} -->
</div>
<div class="mx-auto flex justify-center max-w-7xl h-full">
<ul class="divide-y divide-gray-100">
{#each crimesList as item, crimeListItemIndex}
<!-- <li>
<a
href="/view/{$page.params.vorgang}/{item.name}?pin={vorgangPIN}"
class=" flex justify-between gap-x-6 py-5"
aria-label="zum 3D-modell"
>
<div class=" flex gap-x-4">
<Cube />
<div class="min-w-0 flex-auto">
{#if data?.user?.admin}
<span
id="label__{item.name}"
class="text-sm font-semibold leading-6 text-gray-900 inline-block min-w-1"
contenteditable={!item.show_button}
role="textbox"
tabindex="0"
aria-label="Dateiname bearbeiten"
on:focusout={() => {
defocus_element(crimeListItemIndex);
}}
on:keydown|stopPropagation={// event needed to identify ID
// TO-DO: check if event is needed or if index is sufficient
async (ev) => {
handleEditFieldInput(ev, crimeListItemIndex);
}}>{item.name}</span
>
{#if item.show_button}
<button
style="padding: 2px"
id="edit__{item.name}"
aria-label="Datei umbenennen"
on:click|preventDefault={(ev) => {
let text_field_id = `label__${item.name}`;
let text_field = document.getElementById(text_field_id);
if (text_field) {
text_field.setAttribute('contenteditable', 'true');
text_field.focus();
text_field.textContent = '';
}
// hide button
item.show_button = false;
}}
>
<Edit />
</button>
{/if}
<button
style="padding: 2px"
id="edit__{item.name}"
aria-label="Datei umbenennen"
on:click|preventDefault={(ev) => {
let text_field_id = `label__${item.name}`;
id="del__{item.name}"
on:click|preventDefault={async (ev) => {
let delete_item = window.confirm('Bist du sicher?');
let text_field = document.getElementById(text_field_id);
if (text_field) {
text_field.setAttribute('contenteditable', 'true');
text_field.focus();
text_field.textContent = '';
if (delete_item) {
// bucket: tatort, name: <vorgang>/item-name
let vorgang = $page.params.vorgang;
let filename = '';
if (ev && ev.currentTarget && (ev.currentTarget as HTMLElement).id) {
filename = (ev.currentTarget as HTMLElement).id.split('del__')[1];
}
// delete request
// --------------
let url = new URL($page.url);
url.pathname += `/${filename}`;
try {
const response = await fetch(`/api${url.pathname}`, {
method: 'DELETE'
});
if (response.status == 204) {
setTimeout(() => {
window.location.reload();
}, 500);
}
} catch (error) {
if (error instanceof Error) {
console.log(error.message);
} else {
console.log(error);
}
}
}
// hide button
item.show_button = false;
}}
aria-label="Datei löschen"
>
<Edit />
<Trash />
</button>
{:else}
<span class="text-sm font-semibold leading-6 text-gray-900 inline-block min-w-1"
>{item.name}</span
>
{/if}
<button
style="padding: 2px"
id="del__{item.name}"
on:click|preventDefault={async (ev) => {
let delete_item = window.confirm('Bist du sicher?');
if (delete_item) {
// bucket: tatort, name: <vorgang>/item-name
let vorgang = $page.params.vorgang;
let filename = '';
if (ev && ev.currentTarget && (ev.currentTarget as HTMLElement).id) {
filename = (ev.currentTarget as HTMLElement).id.split('del__')[1];
}
// delete request
// --------------
let url = new URL($page.url);
url.pathname += `/${filename}`;
try {
const response = await fetch(`/api${url.pathname}`, { method: 'DELETE' });
if (response.status == 204) {
setTimeout(() => {
window.location.reload();
}, 500);
}
} catch (error) {
if (error instanceof Error) {
console.log(error.message);
} else {
console.log(error);
}
}
}
}}
aria-label="Datei löschen"
<p class="mt-1 truncate text-xs leading-5 text-gray-500">
{shortenFileSize(item.size)}
</p>
</div>
</div>
<div class="hidden sm:flex sm:flex-col sm:items-end">
<p class="text-sm leading-6 text-gray-900">3D Tatort</p>
<p class="mt-1 text-xs leading-5 text-gray-500">
Zuletzt geändert <time datetime="2023-01-23T13:23Z"
>{timeElapsed(new Date(item.lastModified))}</time
>
<Trash />
</button>
</p>
</div>
</a>
</li> -->
<li>
<div class=" flex gap-x-4">
<a
href="/view/{$page.params.vorgang}/{currentName}?pin={vorgangPIN}"
class=" flex justify-between gap-x-6 py-5"
aria-label="zum 3D-modell"
>
<Cube />
</a>
<div class="min-w-0 flex-auto">
{#if admin}
<EditableItem
list={crimesList}
bind:editedName={names[crimeListItemIndex]}
currentName={item.name}
onSave={handleSave}
onDelete={handleDelete}
></EditableItem>
{:else}
<span class="text-sm font-semibold leading-6 text-gray-900 inline-block min-w-1"
>{item.name}</span
>
{/if}
<p class="mt-1 truncate text-xs leading-5 text-gray-500">
{shortenFileSize(item.size)}
</p>
{#if item.size}
<p class="mt-1 truncate text-xs leading-5 text-gray-500">
{shortenFileSize(item.size)}
</p>
{/if}
</div>
</div>
<div class="hidden sm:flex sm:flex-col sm:items-end">
<p class="text-sm leading-6 text-gray-900">3D Tatort</p>
<p class="mt-1 text-xs leading-5 text-gray-500">
Zuletzt geändert <time datetime="2023-01-23T13:23Z"
>{timeElapsed(new Date(item.lastModified))}</time
>
</p>
{#if item.lastModified}
<p class="mt-1 text-xs leading-5 text-gray-500">
Zuletzt geändert <time datetime="2023-01-23T13:23Z"
>{timeElapsed(new Date(item.lastModified))}</time
>
</p>
{/if}
</div>
</a>
</li>
{/each}
</ul>
</div>
</li>
{/each}
</ul>
</div>
<Modal {open}
><ModalTitle>Umbenennen</ModalTitle><ModalContent>
{#if inProgress}
<p class="py-2 mb-1">Vorgang läuft...</p>
{/if}
{#if err}
<Alert class="w-full" type="error">Fehler beim Umbenennen</Alert>
{/if}
</ModalContent>
<ModalFooter><Button disabled={inProgress} on:click={uploadSuccessful}>Ok</Button></ModalFooter>
</Modal>
</div>
<Modal {open}
><ModalTitle>Umbenennen</ModalTitle><ModalContent>
{#if inProgress}
<p class="py-2 mb-1">Vorgang läuft...</p>
{/if}
{#if err}
<Alert class="w-full" type="error">Fehler beim Umbenennen</Alert>
{/if}
</ModalContent>
<ModalFooter
><Button disabled={inProgress} on:click={uploadSuccessful}>Ok</Button></ModalFooter
>
</Modal>
</div>
{/if}
<style>
ul {

View File

@@ -0,0 +1,10 @@
//Rollenabfrage ob Benutzer admin ist
import { json } from "@sveltejs/kit";
export async function GET({locals}) {
const isAdmin = locals.user?.admin === true;
const data = {admin: isAdmin}
return json(data)
}