Skip to content
Snippets Groups Projects
Commit 0c5d3948 authored by Aaron Dötsch's avatar Aaron Dötsch
Browse files

update

parent 460ad799
Branches
No related tags found
No related merge requests found
Showing with 313 additions and 405 deletions
FROM node:21-alpine FROM node:21-alpine
RUN apk update && apk add build-base g++ cairo-dev pango-dev giflib-dev
WORKDIR /app WORKDIR /app
COPY package*.json ./ COPY package*.json ./
COPY .env ./ COPY .env ./
......
...@@ -2,7 +2,9 @@ version: 3.8 ...@@ -2,7 +2,9 @@ version: 3.8
services: services:
web: web:
build: . build:
context: .
dockerfile: Dockerfile
ports: ports:
- 3000:3000 - 3000:3000
restart: always restart: always
......
This diff is collapsed.
...@@ -83,8 +83,8 @@ ...@@ -83,8 +83,8 @@
<Input name="replyTo" bind:value={replyTo} placeholder="Antwortadresse" class="w-full" {disabled} /> <Input name="replyTo" bind:value={replyTo} placeholder="Antwortadresse" class="w-full" {disabled} />
{:else} {:else}
<Select name="replyTo" value={replyTo} on:change={e=>{ <Select name="replyTo" value={replyTo} on:change={e=>{
customReplyTo = e.target!.value==="custom"; customReplyTo = (e.target! as HTMLSelectElement).value==="custom";
if(!customReplyTo) replyTo = e.target!.value; if(!customReplyTo) replyTo = (e.target! as HTMLSelectElement).value;
else if(replyTo === "-") replyTo = ""; else if(replyTo === "-") replyTo = "";
}} {disabled}> }} {disabled}>
<option value={"-"}>keine</option> <option value={"-"}>keine</option>
...@@ -101,7 +101,7 @@ ...@@ -101,7 +101,7 @@
{#each locales as locale} {#each locales as locale}
<Label class="w-full"> <Label class="w-full">
Betreff ({locale}) Betreff ({locale})
<Input name="subject[{locale}]" bind:value={subject[locale]} oninput={e=>subject=Object.assign(subject,{[locale]:e.target.value})} {disabled} required={locale==="de"} /> <Input name="subject[{locale}]" bind:value={subject[locale]} on:input={e=>subject=Object.assign(subject,{[locale]:(e.target as HTMLInputElement)!.value})} {disabled} required={locale==="de"} />
</Label> </Label>
{/each} {/each}
</div> </div>
...@@ -130,7 +130,7 @@ ...@@ -130,7 +130,7 @@
<Span slot="header">Beispiel-Tutor</Span> <Span slot="header">Beispiel-Tutor</Span>
<Label> <Label>
Studiengang Studiengang
<Select value={exampleTutor.studyProgram.id} on:change={e=>exampleTutor.studyProgram=studyPrograms.find(s=>s.id==e.target!.value)}> <Select value={exampleTutor.studyProgram.id} on:change={e=>exampleTutor.studyProgram=(studyPrograms.find(s=>s.id==parseInt((e.target! as HTMLSelectElement).value))!)}>
{#each studyPrograms as studyProgram} {#each studyPrograms as studyProgram}
<option value={studyProgram.id}>{studyProgram.name[$locale]}</option> <option value={studyProgram.id}>{studyProgram.name[$locale]}</option>
{/each} {/each}
...@@ -160,7 +160,7 @@ ...@@ -160,7 +160,7 @@
</Label> </Label>
<Label> <Label>
Schulung Schulung
<Select value={exampleTutor.training?.id ?? -1} on:change={e=>exampleTutor.training=trainings.find(t=>t.id===e.target!.value)}> <Select value={exampleTutor.training?.id ?? -1} on:change={e=>exampleTutor.training=trainings.find(t=>t.id===(e.target as HTMLSelectElement)!.value)}>
<option value={-1}>keine</option> <option value={-1}>keine</option>
{#each trainings as training} {#each trainings as training}
<option value={training.id}>{training.date}</option> <option value={training.id}>{training.date}</option>
...@@ -182,6 +182,7 @@ ...@@ -182,6 +182,7 @@
</AccordionItem> </AccordionItem>
<AccordionItem> <AccordionItem>
<Span slot="header">Formatierer</Span> <Span slot="header">Formatierer</Span>
<P class="mb-2">Formatierer können mit Argumenten erstellt werden. Dafür nutzt man <code>formatierer:argument</code>, wobei das Argument gültiges JSON sein muss. Es muss darauf geachtet werden, dass sich kein <code>{"}}"}</code> bildet.</P>
<List tag="ul" position="outside" class="ml-4"> <List tag="ul" position="outside" class="ml-4">
{#each Object.entries(formatters) as [name, {description}]} {#each Object.entries(formatters) as [name, {description}]}
<Li><code>{name}</code> - {description}</Li> <Li><code>{name}</code> - {description}</Li>
...@@ -191,7 +192,7 @@ ...@@ -191,7 +192,7 @@
<AccordionItem> <AccordionItem>
<Span slot="header">Bedingungen</Span> <Span slot="header">Bedingungen</Span>
<List tag="ul" position="outside" class="ml-4"> <List tag="ul" position="outside" class="ml-4">
{#each conditions as condition} {#each conditions[type] as condition}
<Li> <Li>
<code>{"{{if "}{condition.name}{#if condition.arguments}{" ...args"}{/if}{"}}"}</code> - {condition.description} <code>{"{{if "}{condition.name}{#if condition.arguments}{" ...args"}{/if}{"}}"}</code> - {condition.description}
{#if condition.arguments} {#if condition.arguments}
...@@ -216,6 +217,7 @@ ...@@ -216,6 +217,7 @@
<b>Formatierung</b> <b>Formatierung</b>
<List tag="ul" position="outside" class="ml-6"> <List tag="ul" position="outside" class="ml-6">
<Li>Werte von Variablen können formatiert werden, indem die Formatierer getrennt durch <code>|</code> nach dem Variablennamen gelistet werden, z.B. <code>{"{{"}variablenname|formatierer1|formatierer2{"}}"}</code>. Die Formatierer werden in der angegebenen Reihenfolge angewandt.</Li> <Li>Werte von Variablen können formatiert werden, indem die Formatierer getrennt durch <code>|</code> nach dem Variablennamen gelistet werden, z.B. <code>{"{{"}variablenname|formatierer1|formatierer2{"}}"}</code>. Die Formatierer werden in der angegebenen Reihenfolge angewandt.</Li>
<Li>Unterstützt ein Formatierer Konstruktorenargumente, so können diese mit <code>:</code> getrennt hinter dem Namen angegeben werden. Das Argument muss gültiges JSON sein und kein frühzeitiges {"}}"} bilden. Beispiel: <code>{"{{"}argument|date:{"{"}"year":"2-digit"{"} "}{"}}"}</code>, wobei das Leerzeichen am Ende wichtig ist, weil sich sonst {"}}}"} bilden würde.</Li>
</List> </List>
</Li> </Li>
<Li> <Li>
......
...@@ -11,28 +11,10 @@ ...@@ -11,28 +11,10 @@
} & Record<string, any>; } & Record<string, any>;
let { items, row, children, ...restProps }: ComponentProps = $props(); let { items, row, children, ...restProps }: ComponentProps = $props();
const sorting = writable({sorted: items, sortDirection: 1, sorter: null}); const sorting = writable({sorted: items, sortDirection: 1, sorter: ""});
setContext("sorting", sorting); setContext("sorting", sorting);
</script> </script>
<style>
div :global(.sortable) {
cursor: pointer;
position: relative;
}
div :global(.sortable::after) {
content: "";
position: absolute;
padding-left: .7rem;
}
div :global(.sorting-asc::after) {
content: "▲";
}
div :global(.sorting-desc::after) {
content: "▼";
}
</style>
<div> <div>
<Table {...restProps}> <Table {...restProps}>
{@render children()} {@render children()}
......
...@@ -5,39 +5,30 @@ ...@@ -5,39 +5,30 @@
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
type T = $$Generic; type T = $$Generic;
const sorting = getContext("sorting") as Writable<{sorted: T[], sortDirection: 1|-1, sorter?: TableHeadCell}>; const sorting = getContext("sorting") as Writable<{sorted: T[], sortDirection: 1|-1, sorter: string}>;
let { sort, children, padding="px-6 py-3", defaultDirection="asc", default: def }: { sort: (a: T, b: T)=>number, children: Snippet, padding?: string, defaultDirection?: "asc"|"desc", default?: boolean } = $props(); let { sort, children, padding="px-6 py-3", defaultDirection="asc", default: def, buttonClass="" }: { sort: (a: T, b: T)=>number, children: Snippet, padding?: string, defaultDirection?: "asc"|"desc", default?: boolean, buttonClass?: string } = $props();
let self: TableHeadCell|undefined = $state(); const self = Math.random().toString(36).substring(2);
if(def){ if(def){
// update directly for SSR
sorting.update(({sorted}) => { sorting.update(({sorted}) => {
let dir = (defaultDirection === "asc" ? 1 : -1) as 1 | -1; let dir = (defaultDirection === "asc" ? 1 : -1) as 1 | -1;
return {sorted: sorted.sort((a,b)=>dir*sort(a,b)), sortDirection: dir, sorter: self}; return {sorted: sorted.sort((a,b)=>dir*sort(a,b)), sortDirection: dir, sorter: self};
}); });
// update on initialization afte "self" has been initialized
onMount(()=>{
sorting.update(({sorted}) => {
let dir = (defaultDirection === "asc" ? 1 : -1) as 1 | -1;
return {sorted: sorted.sort((a,b)=>dir*sort(a,b)), sortDirection: dir, sorter: self};
});
});
} }
function onclick(){ function onclick(){
sorting.update(({sorted, sortDirection, sorter}) => { sorting.update(({sorted, sortDirection, sorter}) => {
if(sorter === self){ sortDirection = (sorter === self ? -sortDirection : defaultDirection === "asc" ? 1 : -1) as 1 | -1;
return {sorted: sorted.sort((a,b)=>-sortDirection*sort(a,b)), sortDirection: -sortDirection as 1 | -1, sorter}; return {sorted: sorted.sort((a,b)=>sortDirection*sort(a,b)), sortDirection, sorter: self};
}
let dir = (defaultDirection === "asc" ? 1 : -1) as 1 | -1;
return {sorted: sorted.sort((a,b)=>dir*sort(a,b)), sortDirection: dir, sorter: self};
}); });
} }
let sortDirection = $derived($sorting.sorter === self ? $sorting.sortDirection === 1 ? "asc" : "desc" : null);
</script> </script>
<TableHeadCell bind:this={self} padding=""> <TableHeadCell padding="" aria-sort={sortDirection ? `${sortDirection}ending` : undefined}>
<button {onclick} class={twMerge(padding, "sortable w-full text-left", $sorting.sorter === self && `sorting-${$sorting.sortDirection === 1 ? "asc": "desc"}`)}> <button {onclick} class={twMerge(padding, "w-full text-left hover:bg-black hover:bg-opacity-10 relative after:absolute after:pl-3", sortDirection === "asc" && "after:content-['▲']", sortDirection === "desc" && "after:content-['▼']", buttonClass)}>
{@render children()} {@render children()}
</button> </button>
</TableHeadCell> </TableHeadCell>
...@@ -5,6 +5,7 @@ import type { Tutor } from "./server/database/entities/Tutor.entity"; ...@@ -5,6 +5,7 @@ import type { Tutor } from "./server/database/entities/Tutor.entity";
import markdownit from "markdown-it"; import markdownit from "markdown-it";
import type { Localized } from "./utils"; import type { Localized } from "./utils";
import type { Config } from "./server/database/entities/Config.entity"; import type { Config } from "./server/database/entities/Config.entity";
import type { DateTimeFormatOptions } from "intl";
const md = markdownit({ const md = markdownit({
breaks: true, breaks: true,
...@@ -21,7 +22,11 @@ export type MailTemplate = { ...@@ -21,7 +22,11 @@ export type MailTemplate = {
type: TemplateType; type: TemplateType;
}; };
export const formatters: Record<string, { description: string, format: (l: Locale, arg: unknown)=>string }> = { export const formatters: Record<string, { description: string, format: (l: Locale, arg: unknown, constructorArg: unknown|null)=>string }> = {
date: {
description: "Formatiert ein Datum, Kontruktorargumente sind die Optionen für Intl.DateTimeFormat (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#options)",
format: (locale, arg, constructorArg)=>new Intl.DateTimeFormat(locale, constructorArg as DateTimeFormatOptions ?? undefined).format(arg as Date),
},
dateLong: { dateLong: {
description: "Formatiert ein Datum, z.B. 01. Januar 1970", description: "Formatiert ein Datum, z.B. 01. Januar 1970",
format: (locale, arg)=>new Intl.DateTimeFormat(locale, { year: "numeric", month: "long", day: "2-digit" }).format(arg as Date), format: (locale, arg)=>new Intl.DateTimeFormat(locale, { year: "numeric", month: "long", day: "2-digit" }).format(arg as Date),
...@@ -38,6 +43,10 @@ export const formatters: Record<string, { description: string, format: (l: Local ...@@ -38,6 +43,10 @@ export const formatters: Record<string, { description: string, format: (l: Local
description: "Gibt den Wochentag eines Datums zurück, z.B. Mo", description: "Gibt den Wochentag eines Datums zurück, z.B. Mo",
format: (locale, arg)=>new Intl.DateTimeFormat(locale, { weekday: "short" }).format(arg as Date), format: (locale, arg)=>new Intl.DateTimeFormat(locale, { weekday: "short" }).format(arg as Date),
}, },
dateRange: {
description: "Formatiert einen Datumsbereich, Konsruktorargumente sind die Optionen für Intl.DateTimeFormat (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#options)",
format: (locale, arg, constructorArg)=>new Intl.DateTimeFormat(locale, constructorArg as DateTimeFormatOptions ?? undefined).formatRange((arg as [Date, Date])[0], (arg as [Date, Date])[1]),
},
dateRangeLong: { dateRangeLong: {
description: "Formatiert einen Datumsbereich, z.B. 1. - 2. Januar 1970", description: "Formatiert einen Datumsbereich, z.B. 1. - 2. Januar 1970",
format: (locale, arg)=>new Intl.DateTimeFormat(locale, { year: "numeric", month: "long", day: "2-digit" }).formatRange((arg as [Date, Date])[0], (arg as [Date, Date])[1]), format: (locale, arg)=>new Intl.DateTimeFormat(locale, { year: "numeric", month: "long", day: "2-digit" }).formatRange((arg as [Date, Date])[0], (arg as [Date, Date])[1]),
...@@ -375,10 +384,15 @@ export function parseTemplate(type: TemplateType, template: Localized|string){ ...@@ -375,10 +384,15 @@ export function parseTemplate(type: TemplateType, template: Localized|string){
const [variableName, ...formatterNames] = name.split("|"); const [variableName, ...formatterNames] = name.split("|");
const variable = variables[type].find(v=>v.name === variableName); const variable = variables[type].find(v=>v.name === variableName);
if(!variable) throw new Error(`Unknown variable ${name}`); if(!variable) throw new Error(`Unknown variable ${name}`);
const fs = formatterNames.map(f=>formatters[f]); const fs = formatterNames.map(f=>{
if(fs.some(f=>!f)) throw new Error(`Unknown formatter ${formatterNames.find((_, i)=>!fs[i])}`); const [formatterName, ...formatterArgs] = f.split(":");
const formatter = formatters[formatterName];
if(!formatter) throw new Error(`Unknown formatter ${formatterName}`);
const arg = formatterArgs.join(":");
return { format: formatter.format, arg: arg ? JSON.parse(arg) : null };
});
const replacement = (t: Tutor, l: Locale, c: PartialConfig)=>{ const replacement = (t: Tutor, l: Locale, c: PartialConfig)=>{
return fs.reduce((v, f)=>f.format(l, v), variable.replacement(t, l, c)); return fs.reduce((v, f)=>f.format(l, v, f.arg), variable.replacement(t, l, c));
}; };
parts.push({type: "variable", replacement}); parts.push({type: "variable", replacement});
} }
...@@ -425,10 +439,15 @@ function consumeCondition(type: TemplateType, tokens: Token[]): [Part[], Part[]] ...@@ -425,10 +439,15 @@ function consumeCondition(type: TemplateType, tokens: Token[]): [Part[], Part[]]
const [variableName, ...formatterNames] = name.split("|"); const [variableName, ...formatterNames] = name.split("|");
const variable = variables[type].find(v=>v.name === variableName); const variable = variables[type].find(v=>v.name === variableName);
if(!variable) throw new Error(`Unknown variable ${name}`); if(!variable) throw new Error(`Unknown variable ${name}`);
const fs = formatterNames.map(f=>formatters[f]); const fs = formatterNames.map(f=>{
if(fs.some(f=>!f)) throw new Error(`Unknown formatter ${formatterNames.find((_, i)=>!fs[i])}`); const [formatterName, ...formatterArgs] = f.split(":");
const formatter = formatters[formatterName];
if(!formatter) throw new Error(`Unknown formatter ${formatterName}`);
const arg = formatterArgs.join(":");
return { format: formatter.format, arg: arg ? JSON.parse(arg) : null };
});
const replacement = (t: Tutor, l: Locale, c: PartialConfig)=>{ const replacement = (t: Tutor, l: Locale, c: PartialConfig)=>{
return fs.reduce((v, f)=>f.format(l, v), variable.replacement(t, l, c)); return fs.reduce((v, f)=>f.format(l, v, f.arg), variable.replacement(t, l, c));
}; };
if(inElse) elseParts.push({type: "variable", replacement}); if(inElse) elseParts.push({type: "variable", replacement});
else thenParts.push({type: "variable", replacement}); else thenParts.push({type: "variable", replacement});
......
...@@ -96,13 +96,13 @@ export const handleAuthorization: Handle = async ({event, resolve})=>{ ...@@ -96,13 +96,13 @@ export const handleAuthorization: Handle = async ({event, resolve})=>{
event.locals.user = user; event.locals.user = user;
}else if(event.url.pathname.startsWith("/intern/tutor/") || event.url.pathname === "/intern/tutor"){ }else if(event.url.pathname.startsWith("/intern/tutor/") || event.url.pathname === "/intern/tutor"){
const session = await event.locals.auth(); const session = await event.locals.auth();
if(!session || session.user.type !== "tutor") redirect(303, "/intern#tutor"); if(!session || session.user.type !== "tutor") redirect(303, `/intern?redirect_url=${encodeURIComponent(event.url.href)}#tutor`);
const tutor = await Tutor.getById(session.user.userId); const tutor = await Tutor.getById(session.user.userId);
if(!tutor) redirect(303, "/intern"); if(!tutor) redirect(303, "/intern");
event.locals.tutor = tutor; event.locals.tutor = tutor;
}else if(event.url.pathname.startsWith("/intern/rallye/") || event.url.pathname === "/intern/rallye"){ }else if(event.url.pathname.startsWith("/intern/rallye/") || event.url.pathname === "/intern/rallye"){
const session = await event.locals.auth(); const session = await event.locals.auth();
if(!session || session.user.type !== "rally") redirect(303, "/intern#rallye"); if(!session || session.user.type !== "rally") redirect(303, `/intern?redirect_url=${encodeURIComponent(event.url.href)}#rallye`);
const supervisor = await RallyStationSupervisor.getById(session.user.userId); const supervisor = await RallyStationSupervisor.getById(session.user.userId);
if(!supervisor) redirect(303, "/intern"); if(!supervisor) redirect(303, "/intern");
event.locals.supervisor = supervisor; event.locals.supervisor = supervisor;
......
...@@ -177,3 +177,20 @@ CREATE TABLE IF NOT EXISTS master_freshers ( ...@@ -177,3 +177,20 @@ CREATE TABLE IF NOT EXISTS master_freshers (
"study_program" TEXT NOT NULL, "study_program" TEXT NOT NULL,
"aachen_experience" TEXT NOT NULL "aachen_experience" TEXT NOT NULL
); );
CREATE TABLE IF NOT EXISTS mrx (
"id" SERIAL PRIMARY KEY
);
CREATE TABLE IF NOT EXISTS mrx_entries (
"id" SERIAL PRIMARY KEY,
"mrx" INT NOT NULL REFERENCES mrx(id),
"date" DATE NOT NULL,
"text" TEXT NOT NULL,
);
CREATE TABLE IF NOT EXISTS mrx_attachments (
"id" SERIAL PRIMARY KEY,
"mrx_entry" INT NOT NULL REFERENCES mrx_entries(id),
"path" TEXT NOT NULL,
);
...@@ -51,8 +51,6 @@ ...@@ -51,8 +51,6 @@
function roundTo5Digits(num: number): number{ function roundTo5Digits(num: number): number{
return Math.round(num * 1e5) / 1e5; return Math.round(num * 1e5) / 1e5;
} }
function refocusMap(address: string): void;
function refocusMap(latitude: number, longitude: number): void;
async function refocusMap(){ async function refocusMap(){
if(arguments.length === 1){ if(arguments.length === 1){
const [address] = arguments; const [address] = arguments;
......
...@@ -5,66 +5,35 @@ ...@@ -5,66 +5,35 @@
import { Permission } from "$lib/perms.js"; import { Permission } from "$lib/perms.js";
import { toCSV } from "$lib/csv"; import { toCSV } from "$lib/csv";
import type { Tutor } from "$lib/server/database/entities/Tutor.entity"; import type { Tutor } from "$lib/server/database/entities/Tutor.entity";
import SortableTable from "$lib/components/SortableTable.svelte";
import SortableTableHeadCell from "$lib/components/SortableTableHeadCell.svelte";
export let data; export let data;
let sortedTutors = data.tutors;
$: dateFormatter = new Intl.DateTimeFormat($locale, { day: "2-digit", month: "2-digit", year: "numeric" }); $: dateFormatter = new Intl.DateTimeFormat($locale, { day: "2-digit", month: "2-digit", year: "numeric" });
type SortProp = string|number|null|undefined;
type SortKey = ObjectToDotProp<typeof data.tutors[number], SortProp>;
let sortKey: SortKey = "firstname";
let sortDirection: -1|1 = 1;
const sortTable = (key: SortKey) => {
if (sortKey === key) {
sortDirection *= -1;
} else {
sortKey = key;
sortDirection = 1;
}
}
$: {
sortedTutors = [...sortedTutors].sort((a, b) => {
const aVal = sortKey!.split(".").reduce((val, key) => val?.[key], a) as unknown as SortProp;
const bVal = sortKey!.split(".").reduce((val, key) => val?.[key], b) as unknown as SortProp;
if(aVal == null && bVal == null) return 0;
else if(aVal == null) return sortDirection;
else if(bVal == null) return -sortDirection;
else if(aVal < bVal) return -sortDirection;
else if(aVal > bVal) return sortDirection;
else return 0;
});
}
function getClassName(key: SortKey, sortKey: SortKey, sortDirection: -1|1){
if(sortKey === key){
return sortDirection === 1 ? "thc sorting-asc" : "thc sorting-desc";
}
return "thc";
}
let showExportModal = false; let showExportModal = false;
let exportType = "json"; let exportType = "json";
const fields: { prop: ObjectToDotProp<Tutor>, label: string, value: string }[] = [ const fields: { prop: (t: Tutor)=>unknown, label: string, value: string }[] = [
{ prop: "firstname", label: "Vorname", value: "firstname" }, { prop: t=>t.firstname, label: "Vorname", value: "firstname" },
{ prop: "lastname", label: "Nachname", value: "lastname" }, { prop: t=>t.lastname, label: "Nachname", value: "lastname" },
{ prop: "nickname", label: "Spitzname", value: "nickname" }, { prop: t=>t.nickname, label: "Spitzname", value: "nickname" },
{ prop: "studyProgram.name.de", label: "Fach", value: "studyProgram" }, { prop: t=>t.studyProgram.name.de, label: "Fach", value: "studyProgram" },
{ prop: "training.date", label: "Schulung", value: "training" }, { prop: t=>t.training?.date, label: "Schulung", value: "training" },
{ prop: "notes", label: "Notiz", value: "notes" }, { prop: t=>t.notes, label: "Notiz", value: "notes" },
...(data.user.permissions.has(Permission.VIEW_TUTOR_DETAILS) ? [ ...(data.user.permissions.has(Permission.VIEW_TUTOR_DETAILS) ? [
{ prop: "gender", label: "Geschlecht", value: "gender" }, { prop: t=>t.gender, label: "Geschlecht", value: "gender" },
{ prop: "mentor", label: "Mentor", value: "mentor" }, { prop: t=>t.mentor, label: "Mentor", value: "mentor" },
{ prop: "shirtSize", label: "Shirt", value: "shirtSize" }, { prop: t=>t.shirtSize, label: "Shirt", value: "shirtSize" },
{ prop: "coTutorWish", label: "Co-Tutor", value: "coTutorWish" }, { prop: t=>t.coTutorWish, label: "Co-Tutor", value: "coTutorWish" },
{ prop: "trained", label: "Geschult", value: "trained" }, { prop: t=>t.trained, label: "Geschult", value: "trained" },
{ prop: "degree", label: "Abschluss", value: "degree" }, { prop: t=>t.degree, label: "Abschluss", value: "degree" },
{ prop: "email", label: "E-Mail", value: "email" }, { prop: t=>t.email, label: "E-Mail", value: "email" },
{ prop: "phone", label: "Telefon", value: "phone" }, { prop: t=>t.phone, label: "Telefon", value: "phone" },
{ prop: "address", label: "Adresse", value: "address" }, { prop: t=>t.address, label: "Adresse", value: "address" },
{ prop: "birthday", label: "Geburtstag", value: "birthday" }, { prop: t=>t.birthday, label: "Geburtstag", value: "birthday" },
{ prop: "dietaryRestriction", label: "Essgewohnheiten", value: "dietaryRestriction" }, { prop: t=>t.dietaryRestriction, label: "Essgewohnheiten", value: "dietaryRestriction" },
] satisfies {prop: ObjectToDotProp<Tutor>, label: string, value: string}[] : []), ] satisfies { prop: (t: Tutor)=>unknown, label: string, value: string }[] : []),
]; ];
let selectedFields: string[] = fields.map(f => f.value); let selectedFields: string[] = fields.map(f => f.value);
function exportTutors(){ function exportTutors(){
...@@ -72,7 +41,7 @@ ...@@ -72,7 +41,7 @@
const tutor: Record<string, unknown> = {}; const tutor: Record<string, unknown> = {};
for(const field of fields){ for(const field of fields){
if(selectedFields.includes(field.value)){ if(selectedFields.includes(field.value)){
tutor[field.value] = field.prop.split(".").reduce((val, key) => val?.[key], t); tutor[field.value] = field.prop(t);
} }
} }
return tutor; return tutor;
...@@ -96,25 +65,6 @@ ...@@ -96,25 +65,6 @@
} }
</script> </script>
<style>
div :global(.thc) {
cursor: pointer;
position: relative;
}
div :global(.thc::after) {
content: "";
position: absolute;
right: 0;
padding-right: 1rem;
}
div :global(.sorting-asc::after) {
content: " ▲";
}
div :global(.sorting-desc::after) {
content: " ▼";
}
</style>
<Breadcrumb class="mb-4"> <Breadcrumb class="mb-4">
<BreadcrumbItem href="/admin" home>Admin</BreadcrumbItem> <BreadcrumbItem href="/admin" home>Admin</BreadcrumbItem>
<BreadcrumbItem href="/admin/tutor">Tutoren</BreadcrumbItem> <BreadcrumbItem href="/admin/tutor">Tutoren</BreadcrumbItem>
...@@ -200,19 +150,25 @@ ...@@ -200,19 +150,25 @@
</Table> </Table>
{#if data.user.permissions.has(Permission.VIEW_TUTORS)} {#if data.user.permissions.has(Permission.VIEW_TUTORS)}
<Table class="mb-6" hoverable> <SortableTable class="mb-6" hoverable items={data.tutors}>
<TableHead> <TableHead>
<TableHeadCell on:click={()=>sortTable("firstname")} class={getClassName("firstname", sortKey, sortDirection)}>Vorname</TableHeadCell> <SortableTableHeadCell sort={(a: Tutor, b)=>a.firstname.localeCompare(b.firstname)}>Vorname</SortableTableHeadCell>
<TableHeadCell on:click={()=>sortTable("lastname")} class={getClassName("lastname", sortKey, sortDirection)}>Nachname</TableHeadCell> <SortableTableHeadCell sort={(a: Tutor, b)=>a.lastname.localeCompare(b.lastname)}>Nachname</SortableTableHeadCell>
<TableHeadCell on:click={()=>sortTable(`studyProgram.name.${$locale}`)} class={getClassName(`studyProgram.name.${$locale}`, sortKey, sortDirection)}>Fach</TableHeadCell> <SortableTableHeadCell sort={(a: Tutor, b)=>a.studyProgram.name[$locale].localeCompare(b.studyProgram.name[$locale])}>Fach</SortableTableHeadCell>
<TableHeadCell on:click={()=>sortTable("shirtSize")} class={getClassName("shirtSize", sortKey, sortDirection)}>Shirt</TableHeadCell> <SortableTableHeadCell sort={(a: Tutor, b)=>a.shirtSize.localeCompare(b.shirtSize)}>T-Shirt</SortableTableHeadCell>
<TableHeadCell on:click={()=>sortTable("coTutorWish")} class={getClassName("coTutorWish", sortKey, sortDirection)}>Co-Tutor</TableHeadCell> <SortableTableHeadCell sort={(a: Tutor, b)=>a.coTutorWish.localeCompare(b.coTutorWish)}>Co-Tutor</SortableTableHeadCell>
<TableHeadCell on:click={()=>sortTable("training.date")} class={getClassName("training.date", sortKey, sortDirection)}>Schulung</TableHeadCell> <SortableTableHeadCell sort={(a: Tutor, b)=>{
<TableHeadCell on:click={()=>sortTable("notes")} class={getClassName("notes", sortKey, sortDirection)}>Notiz</TableHeadCell> if(a.training && b.training) return a.training.date.localeCompare(b.training.date);
else if(a.training) return -1;
else if(b.training) return 1;
else return 0;
}}>Schulung</SortableTableHeadCell>
<SortableTableHeadCell sort={(a: Tutor, b)=>a.notes.localeCompare(b.notes)}>Notiz</SortableTableHeadCell>
{#if data.user.permissions.has(Permission.UPDATE_TUTOR)}
<TableHeadCell></TableHeadCell> <TableHeadCell></TableHeadCell>
{/if}
</TableHead> </TableHead>
<TableBody> {#snippet row({item: tutor})}
{#each sortedTutors as tutor}
<TableBodyRow> <TableBodyRow>
<TableBodyCell>{tutor.firstname}{tutor.nickname?` (${tutor.nickname})`:""}</TableBodyCell> <TableBodyCell>{tutor.firstname}{tutor.nickname?` (${tutor.nickname})`:""}</TableBodyCell>
<TableBodyCell>{tutor.lastname}</TableBodyCell> <TableBodyCell>{tutor.lastname}</TableBodyCell>
...@@ -221,13 +177,12 @@ ...@@ -221,13 +177,12 @@
<TableBodyCell>{tutor.coTutorWish || ""}</TableBodyCell> <TableBodyCell>{tutor.coTutorWish || ""}</TableBodyCell>
<TableBodyCell>{tutor.training?dateFormatter.format(new Date(tutor.training.date)):"Bereits geschult"}</TableBodyCell> <TableBodyCell>{tutor.training?dateFormatter.format(new Date(tutor.training.date)):"Bereits geschult"}</TableBodyCell>
<TableBodyCell>{tutor.notes}</TableBodyCell> <TableBodyCell>{tutor.notes}</TableBodyCell>
{#if data.user.permissions.has(Permission.UPDATE_TUTOR)}
<TableBodyCell> <TableBodyCell>
<Button href={`/admin/tutor/${tutor.id}`} color="light" size="xs">Bearbeiten</Button> <Button href={`/admin/tutor/${tutor.id}`} color="light" size="xs">Bearbeiten</Button>
</TableBodyCell> </TableBodyCell>
{/if}
</TableBodyRow> </TableBodyRow>
{/each} {/snippet}
</TableBody> </SortableTable>
</Table>
{/if} {/if}
<!-- TODO: auto match co-tutors based on wishes ("npm i modern-diacritics" for names) -->
...@@ -65,7 +65,7 @@ A merge request was created to get this issue fixed: https://github.com/nextauth ...@@ -65,7 +65,7 @@ A merge request was created to get this issue fixed: https://github.com/nextauth
} }
}}> }}>
<input type="hidden" name="providerId" value="tutor" /> <input type="hidden" name="providerId" value="tutor" />
<input type="hidden" name="redirectTo" value="/intern/tutor" /> <input type="hidden" name="redirectTo" value={$page.url.searchParams.get("redirect_url") ?? "/intern/tutor"} />
<Input type="email" name="email" placeholder="E-Mail" required class="mb-2" /> <Input type="email" name="email" placeholder="E-Mail" required class="mb-2" />
<Input type="password" name="password" placeholder="Passwort" required class="mb-2" /> <Input type="password" name="password" placeholder="Passwort" required class="mb-2" />
<Button type="submit">Anmelden</Button> <Button type="submit">Anmelden</Button>
...@@ -103,7 +103,7 @@ A merge request was created to get this issue fixed: https://github.com/nextauth ...@@ -103,7 +103,7 @@ A merge request was created to get this issue fixed: https://github.com/nextauth
} }
}}> }}>
<input type="hidden" name="providerId" value="rally" /> <input type="hidden" name="providerId" value="rally" />
<input type="hidden" name="redirectTo" value="/intern/rallye" /> <input type="hidden" name="redirectTo" value={$page.url.searchParams.get("redirect_url") ?? "/intern/rallye"} />
<Input type="email" name="email" placeholder="E-Mail" required class="mb-2" /> <Input type="email" name="email" placeholder="E-Mail" required class="mb-2" />
<Input type="password" name="password" placeholder="Passwort" required class="mb-2" /> <Input type="password" name="password" placeholder="Passwort" required class="mb-2" />
<Button type="submit">Anmelden</Button> <Button type="submit">Anmelden</Button>
......
<script lang="ts"> <script lang="ts">
import SortableTable from "$lib/components/SortableTable.svelte"; import SortableTable from "$lib/components/SortableTable.svelte";
import SortableTableHeadCell from "$lib/components/SortableTableHeadCell.svelte"; import SortableTableHeadCell from "$lib/components/SortableTableHeadCell.svelte";
import { Button, TableBodyCell, TableBodyRow, TableHead, TableHeadCell } from "flowbite-svelte"; import { Button, Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell } from "flowbite-svelte";
let { data } = $props(); let { data } = $props();
...@@ -19,8 +19,8 @@ ...@@ -19,8 +19,8 @@
<SortableTable {items}> <SortableTable {items}>
<TableHead> <TableHead>
<SortableTableHeadCell sort={(a,b)=>a.name.localeCompare(b.name)}>Name</SortableTableHeadCell> <SortableTableHeadCell sort={(a: typeof items[number], b)=>a.name.localeCompare(b.name)}>Name</SortableTableHeadCell>
<SortableTableHeadCell sort={(a,b)=>a.age-b.age} defaultDirection="desc" default>Age</SortableTableHeadCell> <SortableTableHeadCell sort={(a: typeof items[number], b)=>a.age-b.age} defaultDirection="desc">Age</SortableTableHeadCell>
<TableHeadCell>Actions</TableHeadCell> <TableHeadCell>Actions</TableHeadCell>
</TableHead> </TableHead>
{#snippet row({item})} {#snippet row({item})}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment