From b0eefd3ea657c6f3e1f105913998510783c2cc50 Mon Sep 17 00:00:00 2001 From: leider Date: Sun, 16 Mar 2025 17:20:31 +0100 Subject: [PATCH 01/13] typing REST part 1 --- application/backend/batches/batchendpoints.ts | 2 +- .../backend/batches/sendMailToAdmin.ts | 2 +- application/backend/lib/commons/replies.ts | 16 +- application/backend/lib/rider/ridersrest.ts | 3 +- application/backend/rest/calendar.ts | 5 +- application/backend/rest/mail.ts | 5 + application/backend/rest/users.ts | 16 +- application/shared/user/user.ts | 6 +- application/vue/src/commons/auth.tsx | 2 +- .../vue/src/components/mails/MailingLists.tsx | 40 +-- .../options/imageoverview/ImageOverview.tsx | 10 +- .../team/TeamBlock/AdminContent.tsx | 5 +- .../users/TellUserToFillHelpFields.tsx | 4 +- .../vue/src/components/users/UserModals.tsx | 7 +- .../vue/src/components/wiki/WikiPage.tsx | 4 +- .../vue/src/rest/authenticationRequests.ts | 23 ++ application/vue/src/rest/loader.ts | 326 ++++++------------ application/vue/src/widgets/Uploader.tsx | 2 +- 18 files changed, 195 insertions(+), 283 deletions(-) create mode 100644 application/vue/src/rest/authenticationRequests.ts diff --git a/application/backend/batches/batchendpoints.ts b/application/backend/batches/batchendpoints.ts index e584e8cc..44bd001b 100644 --- a/application/backend/batches/batchendpoints.ts +++ b/application/backend/batches/batchendpoints.ts @@ -42,7 +42,7 @@ async function nightlyMails() { checkMaster(now), ]); - const jobtypes: JobType[] = ["Presse", "Fluegel", "Photo", "TextFehlt", "Kasse", "Programmheft", "Staff", "Bar"]; + const jobtypes: JobType[] = ["Presse", "Fluegel", "Photo", "TextFehlt", "Kasse", "Programmheft", "Staff", "Bar", "Master"]; const typedResults = map(results, (jobResult, index) => ({ type: jobtypes[index], jobResult })); return informAdmin(typedResults); } diff --git a/application/backend/batches/sendMailToAdmin.ts b/application/backend/batches/sendMailToAdmin.ts index 6b92923d..84a7f9b1 100644 --- a/application/backend/batches/sendMailToAdmin.ts +++ b/application/backend/batches/sendMailToAdmin.ts @@ -7,7 +7,7 @@ import map from "lodash/map.js"; const receiver = "leider"; -export type JobType = "Programmheft" | "Presse" | "Kasse" | "Bar" | "Photo" | "Fluegel" | "TextFehlt" | "Staff"; +export type JobType = "Programmheft" | "Presse" | "Kasse" | "Bar" | "Photo" | "Fluegel" | "TextFehlt" | "Staff" | "Master"; export async function informAdmin(allResults: { type: JobType; jobResult: JobResult }[]) { const user = userstore.forId(receiver); diff --git a/application/backend/lib/commons/replies.ts b/application/backend/lib/commons/replies.ts index 3cb5c94b..e31031ff 100644 --- a/application/backend/lib/commons/replies.ts +++ b/application/backend/lib/commons/replies.ts @@ -1,17 +1,5 @@ import { Response } from "express"; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function reply(res: Response, err?: Error | null, value?: any): void { - if (err) { - res.status(500).send(err.message ? err.message : err); - return; - } - const valToSend = value?.toJSON ? value.toJSON() : value; - res.type("application/json").send(valToSend || { status: "ok" }); -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function resToJson(res: Response, value?: any) { - const valToSend = value?.toJSON ? value.toJSON() : value; - res.type("application/json").send(valToSend ?? { status: "ok" }); +export function resToJson(res: Response, value?: T) { + res.json(value); } diff --git a/application/backend/lib/rider/ridersrest.ts b/application/backend/lib/rider/ridersrest.ts index b5e0e646..f1758569 100644 --- a/application/backend/lib/rider/ridersrest.ts +++ b/application/backend/lib/rider/ridersrest.ts @@ -25,7 +25,8 @@ app.post("/", (req: Request, res: Response) => { } const anonymous = new User({ id: "anonymous", name: "Rider Anonymous" }); store.saveRider(rider, anonymous); - return resToJson(res, rider); + resToJson(res, rider); + return; } } res.sendStatus(500); diff --git a/application/backend/rest/calendar.ts b/application/backend/rest/calendar.ts index f030d171..1b5926b6 100644 --- a/application/backend/rest/calendar.ts +++ b/application/backend/rest/calendar.ts @@ -3,7 +3,7 @@ import flatMap from "lodash/flatMap.js"; import DatumUhrzeit from "jc-shared/commons/DatumUhrzeit.js"; import { Ical } from "jc-shared/optionen/ferienIcals.js"; -import { TerminFilterOptions } from "jc-shared/optionen/termin.js"; +import { TerminEvent, TerminFilterOptions } from "jc-shared/optionen/termin.js"; import User from "jc-shared/user/user.js"; import store from "../lib/konzerte/konzertestore.js"; @@ -18,6 +18,7 @@ import kalenderEventsService from "../lib/optionen/kalenderEventsService.js"; import Veranstaltung from "jc-shared/veranstaltung/veranstaltung.js"; import map from "lodash/map.js"; import filter from "lodash/filter.js"; +import { identity } from "lodash"; const app = express(); @@ -67,7 +68,7 @@ app.get("/fullcalendarevents.json", async (req, res) => { const termineForIcals = await Promise.all(map(icals, termineForIcal)); const events = termine - .concat(flatMap(termineForIcals, (x) => x)) + .concat(flatMap(termineForIcals, identity)) .concat(konzerte) .concat(vermietungen); resToJson(res, events); diff --git a/application/backend/rest/mail.ts b/application/backend/rest/mail.ts index a0d3e471..9f5deae0 100644 --- a/application/backend/rest/mail.ts +++ b/application/backend/rest/mail.ts @@ -72,4 +72,9 @@ app.post("/mailinglisten", [checkSuperuser], (req: Request, res: Response) => { resToJson(res, users); }); +app.get("/mailinglisten", [checkSuperuser], (req: Request, res: Response) => { + const users = userstore.allUsers(); + resToJson(res, map(users, "withoutPass")); +}); + export default app; diff --git a/application/backend/rest/users.ts b/application/backend/rest/users.ts index 92a882dc..6aeb5ade 100644 --- a/application/backend/rest/users.ts +++ b/application/backend/rest/users.ts @@ -4,28 +4,28 @@ import User from "jc-shared/user/user.js"; import service from "../lib/users/usersService.js"; import store from "../lib/users/userstore.js"; -import { reply, resToJson } from "../lib/commons/replies.js"; +import { resToJson } from "../lib/commons/replies.js"; import { checkCanEditUser, checkSuperuser } from "./checkAccessHandlers.js"; -import invokeMap from "lodash/invokeMap.js"; +import map from "lodash/map.js"; const app = express(); app.get("/users/current", (req, res) => { if (req.user) { - return reply(res, undefined, new User(req.user).toJSONWithoutPass()); + return resToJson(res, new User(req.user).withoutPass); } res.sendStatus(401); }); app.get("/users", (req, res) => { const users = store.allUsers(); - resToJson(res, { users: invokeMap(users, "toJSONWithoutPass") }); + resToJson(res, map(users, "withoutPass")); }); app.post("/user/changePassword", [checkCanEditUser], (req: Request, res: Response) => { const user = new User(req.body); service.changePassword(user, req.user as User); - resToJson(res, user); + resToJson(res, user.withoutPass); }); app.post("/user", [checkCanEditUser], (req: Request, res: Response) => { @@ -37,19 +37,19 @@ app.post("/user", [checkCanEditUser], (req: Request, res: Response) => { user.hashedPassword = existingUser.hashedPassword; user.salt = existingUser.salt; store.save(user, req.user as User); - resToJson(res, user); + resToJson(res, user.withoutPass); }); app.put("/user", [checkSuperuser], (req: Request, res: Response) => { const user = new User(req.body); service.saveNewUserWithPassword(user, req.user as User); - resToJson(res, user); + resToJson(res, user.withoutPass); }); app.delete("/user", [checkSuperuser], (req: Request, res: Response) => { const userToDelete = new User(req.body); store.deleteUser(userToDelete.id, req.user as User); - resToJson(res, userToDelete); + resToJson(res, userToDelete.withoutPass); }); export default app; diff --git a/application/shared/user/user.ts b/application/shared/user/user.ts index e18f1516..57be99e8 100644 --- a/application/shared/user/user.ts +++ b/application/shared/user/user.ts @@ -1,5 +1,6 @@ import Accessrights from "./accessrights.js"; import isNil from "lodash/isNil.js"; +import cloneDeep from "lodash/cloneDeep.js"; export type KannSection = "Kasse" | "Ton" | "Licht" | "Master" | "Ersthelfer"; @@ -49,9 +50,8 @@ export default class User { return result; } - /* eslint-disable-next-line @typescript-eslint/no-explicit-any*/ - toJSONWithoutPass(): any { - const result = this.toJSON(); + get withoutPass() { + const result = cloneDeep(this); delete result.hashedPassword; delete result.salt; return result; diff --git a/application/vue/src/commons/auth.tsx b/application/vue/src/commons/auth.tsx index b197662e..7def320a 100755 --- a/application/vue/src/commons/auth.tsx +++ b/application/vue/src/commons/auth.tsx @@ -3,7 +3,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { LoginState } from "./authConsts"; import { useLocation, useNavigate } from "react-router"; -import { loginPost, logoutManually, refreshTokenPost } from "@/rest/loader.ts"; +import { loginPost, logoutManually, refreshTokenPost } from "@/rest/authenticationRequests"; export interface IUseProvideAuth { /** diff --git a/application/vue/src/components/mails/MailingLists.tsx b/application/vue/src/components/mails/MailingLists.tsx index f7d9aa4a..c40e7757 100644 --- a/application/vue/src/components/mails/MailingLists.tsx +++ b/application/vue/src/components/mails/MailingLists.tsx @@ -1,31 +1,20 @@ import { useQuery } from "@tanstack/react-query"; -import { allUsers, saveMailinglists } from "@/rest/loader.ts"; +import { allMailinglists, saveMailinglists } from "@/rest/loader.ts"; import * as React from "react"; -import { useEffect, useMemo, useState } from "react"; +import { useMemo } from "react"; import { Col } from "antd"; -import Users, { Mailingliste } from "jc-shared/user/users"; +import { Mailingliste } from "jc-shared/user/users"; import EditableTable from "@/widgets/EditableTable/EditableTable.tsx"; import { Columns } from "@/widgets/EditableTable/types.ts"; -import cloneDeep from "lodash/cloneDeep"; import JazzFormAndHeader from "@/components/content/JazzFormAndHeader.tsx"; -import User from "jc-shared/user/user.ts"; import { useJazzMutation } from "@/commons/useJazzMutation.ts"; import map from "lodash/map"; import { JazzRow } from "@/widgets/JazzRow.tsx"; +import { useJazzContext } from "@/components/content/useJazzContext.ts"; -class MailingListsWrapper { - allLists: Mailingliste[] = []; - - constructor(lists?: Mailingliste[]) { - this.allLists = map(lists, (list) => ({ ...list })).sort((a, b) => a.name.localeCompare(b.name)); - } - toJSON(): object { - return cloneDeep(this); - } -} - -function MailingListsInternal({ users }: { readonly users: User[] }) { - const usersAsOptions = useMemo(() => map(users, "asUserAsOption"), [users]); +function MailingListsInternal() { + const { allUsers } = useJazzContext(); + const usersAsOptions = useMemo(() => map(allUsers, "asUserAsOption"), [allUsers]); const columnDescriptions: Columns[] = [ { dataIndex: "name", title: "Name", type: "text", width: "150px", required: true, uniqueValues: true }, @@ -37,7 +26,7 @@ function MailingListsInternal({ users }: { readonly users: User[] }) { columnDescriptions={columnDescriptions} - name="allLists" + name="lists" newRowFactory={(val) => Object.assign({ users: [] }, val)} usersWithKann={usersAsOptions} /> @@ -47,12 +36,7 @@ function MailingListsInternal({ users }: { readonly users: User[] }) { } export default function MailingLists() { - const { data, refetch } = useQuery({ queryKey: ["users"], queryFn: () => allUsers() }); - - const [mailingLists, setMailingLists] = useState(new MailingListsWrapper([])); - useEffect(() => { - setMailingLists(new MailingListsWrapper(new Users(data ?? []).mailinglisten)); - }, [data]); + const { data: mailingLists, refetch } = useQuery({ queryKey: ["mailinglists"], queryFn: allMailinglists }); const mutateLists = useJazzMutation({ saveFunction: saveMailinglists, @@ -60,13 +44,13 @@ export default function MailingLists() { successMessage: "Die Listen wurden gespeichert", }); - function saveForm(vals: MailingListsWrapper) { - mutateLists.mutate(vals.allLists); + function saveForm(vals: { lists: Mailingliste[] }) { + mutateLists.mutate(vals); } return ( - + ); } diff --git a/application/vue/src/components/options/imageoverview/ImageOverview.tsx b/application/vue/src/components/options/imageoverview/ImageOverview.tsx index 2b9462b3..34841ee0 100644 --- a/application/vue/src/components/options/imageoverview/ImageOverview.tsx +++ b/application/vue/src/components/options/imageoverview/ImageOverview.tsx @@ -12,6 +12,10 @@ import { useJazzMutation } from "@/commons/useJazzMutation.ts"; import filter from "lodash/filter"; import { JazzRow } from "@/widgets/JazzRow.tsx"; +function isChanged(v: ImageOverviewRow) { + return v.newname !== v.image; +} + export default function ImageOverview() { useDirtyBlocker(false); const [form] = Form.useForm<{ @@ -39,9 +43,9 @@ export default function ImageOverview() { function saveForm() { const formValues = form.getFieldsValue(true); - const changedRows = filter(formValues.with, (v: ImageOverviewRow) => v.newname !== v.image) - .concat(filter(formValues.notFound, (v: ImageOverviewRow) => v.newname !== v.image)) - .concat(filter(formValues.unused, (v: ImageOverviewRow) => v.newname !== v.image)); + const changedRows: ImageOverviewRow[] = filter(formValues.with, isChanged) + .concat(filter(formValues.notFound, isChanged)) + .concat(filter(formValues.unused, isChanged)); form.validateFields().then(async () => { mutateImages.mutate(changedRows); }); diff --git a/application/vue/src/components/team/TeamBlock/AdminContent.tsx b/application/vue/src/components/team/TeamBlock/AdminContent.tsx index 127d0a90..9b631fe9 100644 --- a/application/vue/src/components/team/TeamBlock/AdminContent.tsx +++ b/application/vue/src/components/team/TeamBlock/AdminContent.tsx @@ -15,6 +15,7 @@ import { useJazzMutation } from "@/commons/useJazzMutation.ts"; import { useJazzContext } from "@/components/content/useJazzContext.ts"; import { useInView } from "react-intersection-observer"; import useFormInstance from "antd/es/form/hooks/useFormInstance"; +import cloneDeep from "lodash/cloneDeep"; type ButtonsProps = { readonly dirty: boolean; @@ -68,10 +69,10 @@ export default function AdminContent({ veranstaltung: veranVermiet }: { readonly }, [veranstaltung]); const setFormValue = useCallback(() => { - const deepCopy = veranstaltung.toJSON(); + const deepCopy = cloneDeep(veranstaltung); form.resetFields(); form.setFieldsValue(deepCopy); - setInitialValue(veranstaltung.toJSON()); + setInitialValue(cloneDeep(veranstaltung)); setDirty(false); }, [form, veranstaltung]); diff --git a/application/vue/src/components/users/TellUserToFillHelpFields.tsx b/application/vue/src/components/users/TellUserToFillHelpFields.tsx index cee64578..ea9a10d7 100644 --- a/application/vue/src/components/users/TellUserToFillHelpFields.tsx +++ b/application/vue/src/components/users/TellUserToFillHelpFields.tsx @@ -8,6 +8,7 @@ import { saveUser } from "@/rest/loader.ts"; import { JazzPageHeader } from "@/widgets/JazzPageHeader.tsx"; import isNil from "lodash/isNil"; import { JazzModal } from "@/widgets/JazzModal.tsx"; +import cloneDeep from "lodash/cloneDeep"; export function TellUserToFillHelpFields() { const { currentUser } = useJazzContext(); @@ -26,8 +27,7 @@ export function TellUserToFillHelpFields() { useEffect(() => { if (currentUser.id) { if (currentUser.hatKeineKannsGefuellt || isNil(currentUser.kannErsthelfer)) { - const deepCopy = currentUser.toJSONWithoutPass(); - form.setFieldsValue(deepCopy); + form.setFieldsValue(cloneDeep(currentUser)); setIsOpen(true); } } diff --git a/application/vue/src/components/users/UserModals.tsx b/application/vue/src/components/users/UserModals.tsx index b46036a7..bdddc582 100644 --- a/application/vue/src/components/users/UserModals.tsx +++ b/application/vue/src/components/users/UserModals.tsx @@ -13,6 +13,7 @@ import { useWatch } from "antd/es/form/Form"; import isNil from "lodash/isNil"; import { logDiffForDirty } from "jc-shared/commons/comparingAndTransforming.ts"; import { JazzModal } from "@/widgets/JazzModal.tsx"; +import cloneDeep from "lodash/cloneDeep"; export function ChangePasswordModal({ isOpen, @@ -36,7 +37,7 @@ export function ChangePasswordModal({ }); useEffect(() => { - form.setFieldsValue(user.toJSONWithoutPass()); + form.setFieldsValue(cloneDeep(user)); }, [form, user]); async function saveForm() { form.validateFields().then(async () => { @@ -137,9 +138,9 @@ export function EditUserModal({ const [dirty, setDirty] = useState(false); function initializeForm() { - const deepCopy = user.toJSONWithoutPass(); + const deepCopy = cloneDeep(user); form.setFieldsValue(deepCopy); - const initial = user.toJSONWithoutPass(); + const initial = cloneDeep(user); setInitialValue(initial); setDirty(areDifferent(initial, deepCopy)); form.validateFields(); diff --git a/application/vue/src/components/wiki/WikiPage.tsx b/application/vue/src/components/wiki/WikiPage.tsx index 0c5f93ea..10169c3e 100644 --- a/application/vue/src/components/wiki/WikiPage.tsx +++ b/application/vue/src/components/wiki/WikiPage.tsx @@ -45,7 +45,7 @@ export default function WikiPage() { }, [data]); const mutateContent = useJazzMutation({ - saveFunction: (content: string) => saveWikiPage(subdir!, realPage, content), + saveFunction: (data: { content: string }) => saveWikiPage(subdir!, realPage, data), queryKey: "wiki", successMessage: "Die Seite wurde gespeichert", }); @@ -73,7 +73,7 @@ export default function WikiPage() { } function saveForm() { form.validateFields().then(async () => { - mutateContent.mutate(form.getFieldValue("content")); + mutateContent.mutate(form.getFieldsValue(true)); setWikipage(form.getFieldValue("content")); }); } diff --git a/application/vue/src/rest/authenticationRequests.ts b/application/vue/src/rest/authenticationRequests.ts new file mode 100644 index 00000000..d8073b1c --- /dev/null +++ b/application/vue/src/rest/authenticationRequests.ts @@ -0,0 +1,23 @@ +import axios from "axios"; + +export async function loginPost(name: string, pass: string) { + const token = await axios.post("/login", { name, pass }); + return refreshTokenPost(token.data.token); +} + +export async function logoutManually() { + return axios.post("/logout"); +} + +export async function refreshTokenPost(tokenFromLogin?: string) { + let token = tokenFromLogin; + if (!tokenFromLogin) { + const result = await axios.post("/refreshToken"); + token = result.data.token; + } + if (!token) { + return ""; + } + axios.defaults.headers.Authorization = `Bearer ${token}`; + return token; +} diff --git a/application/vue/src/rest/loader.ts b/application/vue/src/rest/loader.ts index 95a918f0..58dfbdfe 100644 --- a/application/vue/src/rest/loader.ts +++ b/application/vue/src/rest/loader.ts @@ -1,13 +1,12 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import axios, { AxiosRequestConfig, Method } from "axios"; +import axios, { AxiosRequestConfig, AxiosResponse, Method } from "axios"; import User from "jc-shared/user/user.ts"; import Kalender from "jc-shared/programmheft/kalender.ts"; import OptionValues from "jc-shared/optionen/optionValues.ts"; import Orte from "jc-shared/optionen/orte.ts"; -import { Mailingliste } from "jc-shared/user/users.ts"; +import Users, { Mailingliste } from "jc-shared/user/users.ts"; import MailRule from "jc-shared/mail/mailRule.ts"; -import Termin, { TerminFilterOptions } from "jc-shared/optionen/termin.ts"; +import Termin, { TerminEvent, TerminFilterOptions } from "jc-shared/optionen/termin.ts"; import FerienIcals from "jc-shared/optionen/ferienIcals.ts"; import Konzert, { GastArt, ImageOverviewRow, NameWithNumber } from "jc-shared/konzert/konzert.ts"; import isMobile from "ismobilejs"; @@ -16,44 +15,37 @@ import { Rider } from "jc-shared/rider/rider.ts"; import * as jose from "jose"; import { StaffType } from "jc-shared/veranstaltung/staff.ts"; import Veranstaltung from "jc-shared/veranstaltung/veranstaltung.ts"; -import { HistoryObjectOverview } from "jc-shared/history/history.ts"; +import { HistoryDBType, HistoryObjectOverview } from "jc-shared/history/history.ts"; import { SentMessageInfo } from "nodemailer/lib/smtp-transport"; import map from "lodash/map"; import KonzertWithRiderBoxes from "jc-shared/konzert/konzertWithRiderBoxes.ts"; import { historyFromRawRows } from "@/rest/historyObject.ts"; +import { refreshTokenPost } from "@/rest/authenticationRequests.ts"; +import sortBy from "lodash/sortBy"; -type ContentType = "json" | "pdf" | "zip" | "other"; +type ContentType = "pdf" | "zip" | "other"; -type FetchParams = { +type FetchParams = { url: string; - contentType: ContentType; + contentType?: ContentType; method: Method; - data?: any; + data?: T; + resType?: R; }; -export async function loginPost(name: string, pass: string) { - const token = await axios.post("/login", { name, pass }); - return refreshTokenPost(token.data.token); +async function get(params: Omit, "method">) { + return standardFetch({ ...params, method: "GET" }); } -export async function logoutManually() { - return axios.post("/logout"); +async function loeschen(params: Omit, "method">) { + return standardFetch({ ...params, method: "DELETE" }); } -export async function refreshTokenPost(tokenFromLogin?: string) { - let token = tokenFromLogin; - if (!tokenFromLogin) { - const result = await axios.post("/refreshToken"); - token = result.data.token; - } - if (!token) { - return ""; - } - axios.defaults.headers.Authorization = `Bearer ${token}`; - return token; +async function post(params: Omit, "method">) { + return standardFetch({ ...params, method: "POST" }); } -async function standardFetch(params: FetchParams) { +async function standardFetch(params: FetchParams) { if (!axios.defaults.headers.Authorization) { await refreshTokenPost(); } else { @@ -65,55 +57,46 @@ async function standardFetch(params: FetchParams) { console.log("token veraltet"); } } - const options: AxiosRequestConfig = { + const options: AxiosRequestConfig = { url: params.url, method: params.method, data: params.data, - responseType: params.contentType !== "json" ? "blob" : "json", + responseType: params.contentType ? "blob" : "json", }; - const res = await axios(options); + const res = await axios>(options); return res.data; } -async function getForType(contentType: ContentType, url: string) { - return standardFetch({ contentType, url, method: "GET" }); -} - export async function uploadFile(data: FormData) { - return standardFetch({ - method: "POST", - url: "/rest/upload", - data, - contentType: "json", - }); + const result = await post({ url: "/rest/upload", data, resType: new Konzert() }); + return new Konzert(result); } export async function uploadWikiImage(data: FormData) { - const res = await standardFetch({ - method: "POST", + return await post({ url: "/rest/wiki/upload", data, - contentType: "json", + + resType: { url: "" }, }); - return res as { url: string }; } -function handleVeranstaltungen(result?: any[]): Konzert[] { - return map(result, (each: any) => new Konzert(each)) || []; +function handleVeranstaltungen(result?: Konzert[]): Konzert[] { + return map(result, (each) => new Konzert(each)); } export async function konzerteBetweenYYYYMM(start: string, end: string) { - const result = await getForType("json", `/rest/konzerte/${start}/${end}`); + const result = await get({ url: `/rest/konzerte/${start}/${end}`, resType: [new Konzert()] }); return handleVeranstaltungen(result); } export async function konzerteForToday() { - const result = await getForType("json", `/rest/konzerte/fortoday`); + const result = await get({ url: `/rest/konzerte/fortoday`, resType: [new Konzert()] }); return handleVeranstaltungen(result); } export async function konzerteForTeam(selector: "zukuenftige" | "vergangene" | "alle") { - const result = await getForType("json", `/rest/konzerte/${selector}`); + const result = await get({ url: `/rest/konzerte/${selector}`, resType: [new Konzert()] }); return handleVeranstaltungen(result); } @@ -124,7 +107,7 @@ export async function konzertWithRiderForUrl(url: string): Promise new Vermietung(each)) || []; +function handleVermietungen(result?: Vermietung[]): Vermietung[] { + return map(result, (each) => new Vermietung(each)); } export async function vermietungenForTeam(selector: "zukuenftige" | "vergangene" | "alle") { - const result = await getForType("json", `/rest/vermietungen/${selector}`); + const result = await get({ url: `/rest/vermietungen/${selector}`, resType: [new Vermietung()] }); return handleVermietungen(result); } export async function vermietungenBetweenYYYYMM(start: string, end: string) { - const result = await getForType("json", `/rest/vermietungen/${start}/${end}`); + const result = await get({ url: `/rest/vermietungen/${start}/${end}`, resType: [new Vermietung()] }); return handleVermietungen(result); } @@ -198,7 +179,7 @@ export async function vermietungForUrl(url: string): Promise { } if (url.startsWith("copy-of-")) { const realUrl = url.substring(8); - const result = await getForType("json", `/rest/vermietung/${encodeURIComponent(realUrl)}`); + const result = await get({ url: `/rest/vermietung/${encodeURIComponent(realUrl)}`, resType: new Vermietung() }); if (result) { const vermietung = new Vermietung(result); vermietung.reset(); @@ -207,33 +188,23 @@ export async function vermietungForUrl(url: string): Promise { return result; } } - const result = await getForType("json", `/rest/vermietung/${encodeURIComponent(url)}`); + const result = await get({ url: `/rest/vermietung/${encodeURIComponent(url)}`, resType: new Vermietung() }); return result ? new Vermietung(result) : result; } export async function saveVermietung(vermietung: Vermietung) { - const result = await standardFetch({ - method: "POST", - url: "/rest/vermietung", - data: vermietung.toJSON(), - contentType: "json", - }); + const result = await post({ url: "/rest/vermietung", data: vermietung.toJSON(), resType: vermietung }); return new Vermietung(result); } export async function deleteVermietungWithId(id: string) { - return standardFetch({ - method: "DELETE", - url: "/rest/vermietung", - data: { id }, - contentType: "json", - }); + return loeschen({ url: "/rest/vermietung", data: { id } }); } // User export async function currentUser() { try { - const result = await getForType("json", "/rest/users/current"); + const result = await get({ url: "/rest/users/current", resType: {} as User }); return new User(result); } catch { return new User({ id: "invalidUser" }); @@ -241,221 +212,154 @@ export async function currentUser() { } export async function allUsers(): Promise { - const result = await getForType("json", "/rest/users"); - return map(result?.users, (user: any) => new User(user)) || []; + const result = await get({ url: "/rest/users", resType: [{} as User] }); + return map(result, (user) => new User(user)); } export async function saveUser(user: User) { - return standardFetch({ - method: "POST", - url: "/rest/user", - data: user.toJSON(), - contentType: "json", - }); + const result = await post({ url: "/rest/user", data: user.toJSON(), resType: user }); + return new User(result); } export async function deleteUser(user: User) { - return standardFetch({ - method: "DELETE", - url: "/rest/user", - data: user.toJSON(), - contentType: "json", - }); + return loeschen({ url: "/rest/user", data: user.toJSON() }); } export async function saveNewUser(user: User) { - return standardFetch({ - method: "PUT", - url: "/rest/user", - data: user.toJSON(), - contentType: "json", - }); + return standardFetch({ method: "PUT", url: "/rest/user", data: user.toJSON() }); } export async function changePassword(user: User) { - return standardFetch({ - method: "POST", - url: "/rest/user/changePassword", - data: user.toJSON(), - contentType: "json", - }); + const result = await post({ url: "/rest/user/changePassword", data: user.toJSON(), resType: user }); + return new User(result); } // Programmheft export async function kalenderFor(jahrMonat: string) { - const result = await getForType("json", `/rest/programmheft/${jahrMonat}`); + const result = await get({ url: `/rest/programmheft/${jahrMonat}`, resType: new Kalender() }); return result?.id ? new Kalender(result) : new Kalender({ id: jahrMonat }); } -export async function alleKalender(): Promise { - const result = await getForType("json", "/rest/programmheft/alle"); - return result.length > 0 ? map(result, (r: any) => new Kalender(r)) : []; +export async function alleKalender() { + const result = await get({ url: "/rest/programmheft/alle", resType: [new Kalender()] }); + return result.length > 0 ? map(result, (r) => new Kalender(r)) : []; } export async function saveProgrammheft(kalender: Kalender) { - return standardFetch({ - method: "POST", - url: "/rest/programmheft", - data: kalender, - contentType: "json", - }); + const result = await post({ url: "/rest/programmheft", data: kalender }); + return new Kalender(result); } // Rider export async function saveRider(rider: Rider) { - const result = await standardFetch({ - method: "POST", - url: "/rest/riders", - data: rider, - contentType: "json", - }); + const result = await post({ url: "/rest/riders", data: rider }); return new Rider(result); } // Optionen & Termine export async function optionen(): Promise { - const result = await getForType("json", "/rest/optionen"); + const result = await get({ url: "/rest/optionen", resType: new OptionValues() }); return result ? new OptionValues(result) : result; } export async function saveOptionen(optionen: OptionValues) { - const result = await standardFetch({ - method: "POST", - url: "/rest/optionen", - data: optionen.toJSON(), - contentType: "json", - }); + const result = await post({ url: "/rest/optionen", data: optionen.toJSON(), resType: optionen }); return new OptionValues(result); } export async function orte() { - const result = await getForType("json", "/rest/orte"); + const result = await get({ url: "/rest/orte", resType: new Orte() }); return result ? new Orte(result) : result; } export async function saveOrte(orte: Orte) { - return standardFetch({ - method: "POST", - url: "/rest/orte", - data: orte.toJSON(), - contentType: "json", - }); + const result = await post({ url: "/rest/orte", data: orte.toJSON(), resType: orte }); + return new Orte(result); } -export async function termine(): Promise { - const result = await getForType("json", "/rest/termine"); - return map(result, (r: any) => new Termin(r)) ?? []; +export async function termine() { + const result = await get({ url: "/rest/termine", resType: [new Termin()] }); + return map(result, (r) => new Termin(r)) ?? []; } export async function saveTermine(termine: Termin[]) { - const result = await standardFetch({ - method: "POST", - url: "/rest/termine", - data: termine, - contentType: "json", - }); - return map(result, (r: any) => new Termin(r)) ?? []; + const result = await post({ url: "/rest/termine", data: termine }); + return map(result, (r) => new Termin(r)) ?? []; } export async function kalender() { - const result = await getForType("json", "/rest/kalender"); + const result = await get({ url: "/rest/kalender", resType: new FerienIcals() }); return result ? new FerienIcals(result) : result; } export async function saveKalender(kalender: FerienIcals) { - const result = await standardFetch({ - method: "POST", - url: "/rest/kalender", - data: kalender, - contentType: "json", - }); + const result = await post({ url: "/rest/kalender", data: kalender }); return result ? new FerienIcals(result) : result; } // Image export async function imagenames() { - const result = await getForType("json", "/rest/imagenames"); - return (result?.names as string[]) || []; + const result = await get({ url: "/rest/imagenames", resType: { names: [""] } }); + return result?.names ?? []; } export async function saveImagenames(rows: ImageOverviewRow[]) { - return standardFetch({ - method: "POST", - url: "/rest/imagenames", - data: rows, - contentType: "json", - }); + await post({ url: "/rest/imagenames", data: rows, resType: { names: [""] } }); + return rows; } //Mails intern export async function sendMail(formData: FormData) { - return standardFetch({ - method: "POST", - url: "/rest/rundmail", - data: formData, - contentType: "json", - }) as Promise; -} - -export async function saveMailinglists(lists: Mailingliste[]) { - return standardFetch({ - method: "POST", - url: "/rest/mailinglisten", - data: lists, - contentType: "json", - }); + return post({ url: "/rest/rundmail", data: formData, resType: {} as SentMessageInfo }); +} + +export async function allMailinglists() { + const result = await get({ url: "/rest/mailinglisten", resType: [] as User[] }); + return { lists: sortBy(new Users(result ?? []).mailinglisten, "name") }; +} + +export async function saveMailinglists({ lists }: { lists: Mailingliste[] }) { + const result = await post({ url: "/rest/mailinglisten", data: lists, resType: [] as User[] }); + return { lists: sortBy(new Users(result ?? []).mailinglisten, "name") }; } // Mails für Veranstaltungen -export async function mailRules(): Promise { - const result = await getForType("json", "/rest/mailrule"); - return map(result, (each: any) => new MailRule(each)) || []; +export async function mailRules() { + const result = await get({ url: "/rest/mailrule", resType: [new MailRule()] }); + return map(result, (each) => new MailRule(each)); } export async function saveMailRules(rules: MailRule[]) { - return standardFetch({ - method: "POST", - url: "/rest/mailrules", - data: rules, - contentType: "json", - }); + return post({ url: "/rest/mailrules", data: rules }); } // Wiki -export async function wikisubdirs(): Promise<{ dirs: string[] }> { - const json = await getForType("json", "/rest/wikidirs"); - return json || { dirs: [] }; +export async function wikisubdirs() { + const json = await get({ url: "/rest/wikidirs", resType: { dirs: [""] } }); + return json ?? { dirs: [] }; } export async function wikiPage(subdir: string, page: string) { - const result = await getForType("json", `/rest/wikipage/${subdir}/${page}`); - return result?.content || ""; + const result = await get({ url: `/rest/wikipage/${subdir}/${page}`, resType: { content: "" } }); + return result?.content ?? ""; } -export async function saveWikiPage(subdir: string, page: string, content: string) { - return standardFetch({ - method: "POST", - url: `/rest/wikipage/${subdir}/${page}`, - data: { content }, - contentType: "json", - }); +export async function saveWikiPage(subdir: string, page: string, data: { content: string }) { + return post({ url: `/rest/wikipage/${subdir}/${page}`, data }); } export async function searchWiki(suchtext: string) { - return standardFetch({ - method: "POST", + return post({ url: "/rest/wikipage/search", data: { suchtext }, - contentType: "json", + resType: { searchtext: "", matches: [{ pageName: "", line: "", text: "" }] }, }); } export async function deleteWikiPage(subdir: string, page: string) { - return standardFetch({ - method: "DELETE", + return loeschen({ url: `/rest/wikipage/${subdir}/${page}`, data: { data: "" }, - contentType: "json", }); } @@ -475,17 +379,17 @@ export async function calendarEventSources({ if (options) { segments.push(`&options=${JSON.stringify(options)}`); } - return getForType("json", segments.join()); + return get({ url: segments.join(), resType: [{} as TerminEvent] }); } // History export async function historyIdsFor(collection: string) { - const result = await getForType("json", `/rest/history/${collection}`); + const result = await get({ url: `/rest/history/${collection}` }); return result as HistoryObjectOverview[]; } export async function historyRowsFor(collection: string, id: string) { - const result = await getForType("json", `/rest/history/${collection}/${encodeURIComponent(id)}`); + const result = await get({ url: `/rest/history/${collection}/${encodeURIComponent(id)}`, resType: [{} as HistoryDBType] }); return historyFromRawRows(result); } @@ -508,15 +412,15 @@ export async function openAngebotRechnung(vermietung: Vermietung) { window.open(`/pdf/vermietungAngebot/${encodeURIComponent(filename)}?url=${encodeURIComponent(vermietung.url!)}&art=${vermietung.art}`); } -export async function imgFullsize(url: any) { - const img = await getForType("other", `/upload/${url}`); +export async function imgFullsize(url: string) { + const img = await get({ contentType: "other", url: `/upload/${url}`, resType: new Blob() }); if (img) { showFile(img, url); } } export async function imgzipForVeranstaltung(konzert: Konzert) { - const zip = await getForType("zip", `/imgzipForVeranstaltung/${konzert.url}`); + const zip = await get({ contentType: "zip", url: `/imgzipForVeranstaltung/${konzert.url}`, resType: new Blob() }); if (zip) { showFile(zip, `JazzClub_Bilder_${konzert.kopf.titel}.zip`); } diff --git a/application/vue/src/widgets/Uploader.tsx b/application/vue/src/widgets/Uploader.tsx index fa66e515..05f4e8d8 100644 --- a/application/vue/src/widgets/Uploader.tsx +++ b/application/vue/src/widgets/Uploader.tsx @@ -42,7 +42,7 @@ export default function Uploader({ name, typ, onlyImages = false }: UploaderPara try { const newVeranstaltung = await uploadFile(formData); setFileList([]); - const strings = name.reduce((prev, curr) => prev[curr], newVeranstaltung); + const strings = name.reduce((prev, curr) => (prev as any)[curr], newVeranstaltung); form.setFieldValue(name, strings); } catch { // eslint-disable-next-line no-console From ea75f7d2d9ab51f6524f7647f174da0371c02303 Mon Sep 17 00:00:00 2001 From: leider Date: Thu, 20 Mar 2025 16:18:34 +0100 Subject: [PATCH 02/13] typing REST part 2 --- .../backend/lib/konzerte/konzertestore.ts | 3 +-- .../backend/lib/mailsender/mailstore.ts | 2 +- .../lib/optionen/kalendereventstore.ts | 2 +- .../backend/lib/optionen/optionenstore.ts | 2 +- .../backend/lib/optionen/terminstore.ts | 2 +- .../lib/persistence/sqlitePersistence.ts | 13 ++++++---- application/backend/lib/rider/riderstore.ts | 2 +- .../lib/vermietungen/vermietungenstore.ts | 3 +-- application/backend/rest/calendar.ts | 2 +- application/backend/rest/konzerte.ts | 3 +-- application/backend/rest/mail.ts | 7 ++---- application/backend/rest/optionen.ts | 3 +-- application/backend/rest/vermietungen.ts | 3 +-- application/shared/konzert/eintrittspreise.ts | 5 ---- application/shared/konzert/kasse.ts | 4 --- application/shared/konzert/konzert.ts | 19 -------------- application/shared/konzert/unterkunft.ts | 5 ---- application/shared/konzert/vertrag.ts | 5 ---- application/shared/mail/mailRule.ts | 5 ---- application/shared/optionen/ferienIcals.ts | 18 ------------- application/shared/optionen/optionValues.ts | 4 --- application/shared/optionen/orte.ts | 11 -------- application/shared/optionen/termin.ts | 5 ---- application/shared/rider/rider.ts | 4 --- .../konzert/eintrittspreise_object.test.ts | 22 ++++++++-------- application/shared/user/user.ts | 13 ++-------- application/shared/veranstaltung/artist.ts | 4 --- application/shared/veranstaltung/kontakt.ts | 5 ---- application/shared/veranstaltung/kopf.ts | 5 ---- application/shared/veranstaltung/kosten.ts | 5 ---- application/shared/veranstaltung/presse.ts | 5 ---- application/shared/veranstaltung/staff.ts | 4 --- application/shared/veranstaltung/technik.ts | 5 ---- .../shared/veranstaltung/veranstaltung.ts | 2 -- application/shared/vermietung/angebot.ts | 5 ---- application/shared/vermietung/vermietung.ts | 15 ----------- .../vue/src/components/mails/MailRules.tsx | 7 +----- .../src/components/options/TerminePage.tsx | 4 --- application/vue/src/rest/loader.ts | 25 ++++++++----------- application/vue/src/widgets/Uploader.tsx | 1 + 40 files changed, 46 insertions(+), 213 deletions(-) diff --git a/application/backend/lib/konzerte/konzertestore.ts b/application/backend/lib/konzerte/konzertestore.ts index 1a07f2c6..13ffcdd8 100644 --- a/application/backend/lib/konzerte/konzertestore.ts +++ b/application/backend/lib/konzerte/konzertestore.ts @@ -60,8 +60,7 @@ export default { }, saveKonzert(konzert: Konzert, user: User) { - const object = konzert.toJSON(); - persistence.save(object as { id: string }, user); + persistence.save(konzert as { id: string }, user); return konzert; }, diff --git a/application/backend/lib/mailsender/mailstore.ts b/application/backend/lib/mailsender/mailstore.ts index 5b7b9dd8..c5970469 100644 --- a/application/backend/lib/mailsender/mailstore.ts +++ b/application/backend/lib/mailsender/mailstore.ts @@ -21,7 +21,7 @@ export default { }, save: function save(mailRule: MailRule, user: User) { - persistence.save(mailRule.toJSON(), user); + persistence.save(mailRule, user); return mailRule; }, diff --git a/application/backend/lib/optionen/kalendereventstore.ts b/application/backend/lib/optionen/kalendereventstore.ts index a2bd90b3..a0f8cfa0 100644 --- a/application/backend/lib/optionen/kalendereventstore.ts +++ b/application/backend/lib/optionen/kalendereventstore.ts @@ -12,7 +12,7 @@ export default { }, save: function save(event: KalenderEvents) { - persistence.save(event.toJSON(), new User({ name: "System" })); + persistence.save(event, new User({ name: "System" })); return event; }, }; diff --git a/application/backend/lib/optionen/optionenstore.ts b/application/backend/lib/optionen/optionenstore.ts index dd21772b..048774c6 100644 --- a/application/backend/lib/optionen/optionenstore.ts +++ b/application/backend/lib/optionen/optionenstore.ts @@ -25,7 +25,7 @@ export default { }, save: function save(object: OptionValues | Orte | FerienIcals, user: User) { - persistence.save(object.toJSON ? object.toJSON() : object, user); + persistence.save(object, user); return object; }, }; diff --git a/application/backend/lib/optionen/terminstore.ts b/application/backend/lib/optionen/terminstore.ts index 7f9cbf7c..4b1fddab 100644 --- a/application/backend/lib/optionen/terminstore.ts +++ b/application/backend/lib/optionen/terminstore.ts @@ -23,7 +23,7 @@ export default { }, save: function save(termin: Termin, user: User) { - persistence.save(termin.toJSON(), user); + persistence.save(termin, user); return termin; }, diff --git a/application/backend/lib/persistence/sqlitePersistence.ts b/application/backend/lib/persistence/sqlitePersistence.ts index b02f9c38..9933f63c 100644 --- a/application/backend/lib/persistence/sqlitePersistence.ts +++ b/application/backend/lib/persistence/sqlitePersistence.ts @@ -5,6 +5,7 @@ import User from "jc-shared/user/user.js"; import { areDifferentForHistoryEntries } from "jc-shared/commons/comparingAndTransforming.js"; import map from "lodash/map.js"; import forEach from "lodash/forEach.js"; +import isString from "lodash/isString.js"; export const db = new Database(conf.sqlitedb); const scriptLogger = loggers.get("scripts"); @@ -14,8 +15,11 @@ function asSqliteString(obj: object) { return `${escape(JSON.stringify(obj))}`; } -export function escape(str = "") { - return `'${str.replaceAll("'", "''")}'`; +export function escape(str: string | Date = "") { + if (isString(str)) { + return `'${str.replaceAll("'", "''")}'`; + } + return `'${str.toJSON().replaceAll("'", "''")}'`; } export function execWithTry(command: string) { @@ -93,11 +97,10 @@ class Persistence { return ["id", "data"].concat(this.extraCols); } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - private createValsForSave(object: { [ind: string]: any } & { id: string }) { + private createValsForSave(object: { [ind: string]: string | Date } & { id: string }) { return [escape(object.id), asSqliteString(object)].concat( map(this.extraCols, (col) => { - return object[col]?.toJSON ? escape(object[col].toJSON()) : escape(object[col]); + return escape(object[col]); }), ); } diff --git a/application/backend/lib/rider/riderstore.ts b/application/backend/lib/rider/riderstore.ts index 03932cf0..edf5d74c 100644 --- a/application/backend/lib/rider/riderstore.ts +++ b/application/backend/lib/rider/riderstore.ts @@ -12,7 +12,7 @@ export default { }, saveRider: function saveRider(object: Rider, user: User) { - persistence.save(object.toJSON() as Rider, user); + persistence.save(object, user); return object; }, }; diff --git a/application/backend/lib/vermietungen/vermietungenstore.ts b/application/backend/lib/vermietungen/vermietungenstore.ts index 2f60347f..ac179b79 100644 --- a/application/backend/lib/vermietungen/vermietungenstore.ts +++ b/application/backend/lib/vermietungen/vermietungenstore.ts @@ -60,8 +60,7 @@ export default { }, saveVermietung: function saveVermietung(vermietung: Vermietung, user: User) { - const object = vermietung.toJSON(); - persistence.save(object as { id: string }, user); + persistence.save(vermietung as { id: string }, user); return vermietung; }, diff --git a/application/backend/rest/calendar.ts b/application/backend/rest/calendar.ts index 1b5926b6..2047166f 100644 --- a/application/backend/rest/calendar.ts +++ b/application/backend/rest/calendar.ts @@ -18,7 +18,7 @@ import kalenderEventsService from "../lib/optionen/kalenderEventsService.js"; import Veranstaltung from "jc-shared/veranstaltung/veranstaltung.js"; import map from "lodash/map.js"; import filter from "lodash/filter.js"; -import { identity } from "lodash"; +import identity from "lodash/identity.js"; const app = express(); diff --git a/application/backend/rest/konzerte.ts b/application/backend/rest/konzerte.ts index 12436e4b..f1dd3fcc 100644 --- a/application/backend/rest/konzerte.ts +++ b/application/backend/rest/konzerte.ts @@ -12,13 +12,12 @@ import { kassenzettelToBuchhaltung } from "../lib/pdf/pdfGeneration.js"; import { checkAbendkasse, checkOrgateam } from "./checkAccessHandlers.js"; import parseFormData from "../lib/commons/parseFormData.js"; import find from "lodash/find.js"; -import invokeMap from "lodash/invokeMap.js"; const app = express(); function standardHandler(res: Response, user: User, konzerte: Konzert[]) { const result = konzerteService.filterUnbestaetigteFuerJedermann(konzerte, user); - resToJson(res, invokeMap(result, "toJSON")); + resToJson(res, result); } function saveAndReply(req: Request, res: Response, konzert: Konzert) { diff --git a/application/backend/rest/mail.ts b/application/backend/rest/mail.ts index 9f5deae0..22c86223 100644 --- a/application/backend/rest/mail.ts +++ b/application/backend/rest/mail.ts @@ -13,7 +13,6 @@ import User from "jc-shared/user/user.js"; import fs from "fs/promises"; import parseFormData from "../lib/commons/parseFormData.js"; import MailMessage from "jc-shared/mail/mailMessage.js"; -import invokeMap from "lodash/invokeMap.js"; import map from "lodash/map.js"; import forEach from "lodash/forEach.js"; import filter from "lodash/filter.js"; @@ -21,15 +20,13 @@ import filter from "lodash/filter.js"; const app = express(); app.get("/mailrule", [checkSuperuser], (req: Request, res: Response) => { - const rules = mailstore.all(); - const result = invokeMap(rules, "toJSON"); - resToJson(res, result); + resToJson(res, mailstore.all()); }); app.post("/mailrules", [checkSuperuser], (req: Request, res: Response) => { const oldRules = mailstore.all(); const newRules = misc.toObjectList(MailRule, req.body); - const { changed, deletedIds } = calculateChangedAndDeleted(invokeMap(newRules, "toJSON"), invokeMap(oldRules, "toJSON")); + const { changed, deletedIds } = calculateChangedAndDeleted(newRules, oldRules); mailstore.saveAll(changed, req.user as User); mailstore.removeAll(deletedIds, req.user as User); diff --git a/application/backend/rest/optionen.ts b/application/backend/rest/optionen.ts index c73a473e..eae59f3e 100644 --- a/application/backend/rest/optionen.ts +++ b/application/backend/rest/optionen.ts @@ -12,7 +12,6 @@ import { calculateChangedAndDeleted } from "jc-shared/commons/compareObjects.js" import misc from "jc-shared/commons/misc.js"; import { checkOrgateam } from "./checkAccessHandlers.js"; import User from "jc-shared/user/user.js"; -import invokeMap from "lodash/invokeMap.js"; const app = express(); @@ -51,7 +50,7 @@ app.get("/termine", (req, res) => { app.post("/termine", [checkOrgateam], (req: Request, res: Response) => { const oldTermine = terminstore.alle(); const newTermine = misc.toObjectList(Termin, req.body); - const { changed, deletedIds } = calculateChangedAndDeleted(invokeMap(newTermine, "toJSON"), invokeMap(oldTermine, "toJSON")); + const { changed, deletedIds } = calculateChangedAndDeleted(newTermine, oldTermine); terminstore.saveAll(changed, req.user as User); terminstore.removeAll(deletedIds, req.user as User); resToJson(res, terminstore.alle()); diff --git a/application/backend/rest/vermietungen.ts b/application/backend/rest/vermietungen.ts index 8dbdf435..5d5b7401 100644 --- a/application/backend/rest/vermietungen.ts +++ b/application/backend/rest/vermietungen.ts @@ -9,13 +9,12 @@ import store from "../lib/vermietungen/vermietungenstore.js"; import { checkOrgateam } from "./checkAccessHandlers.js"; import { saveVermietungToShare, vermietungVertragToBuchhaltung } from "../lib/pdf/pdfGeneration.js"; import { filterUnbestaetigteFuerJedermann } from "../lib/vermietungen/vermietungenService.js"; -import invokeMap from "lodash/invokeMap.js"; const app = express(); async function standardHandler(req: Request, res: Response, vermietungen: Vermietung[]) { const user: User = req.user as User; - resToJson(res, invokeMap(filterUnbestaetigteFuerJedermann(vermietungen, user), "toJSON")); + resToJson(res, filterUnbestaetigteFuerJedermann(vermietungen, user)); } function saveAndReply(req: Request, res: Response, vermietung: Vermietung) { diff --git a/application/shared/konzert/eintrittspreise.ts b/application/shared/konzert/eintrittspreise.ts index 12970d0d..a384e5fa 100644 --- a/application/shared/konzert/eintrittspreise.ts +++ b/application/shared/konzert/eintrittspreise.ts @@ -23,11 +23,6 @@ export default class Eintrittspreise { }; } - /* eslint-disable-next-line @typescript-eslint/no-explicit-any*/ - toJSON(): any { - return Object.assign({}, this); - } - constructor(object?: RecursivePartial) { if (object && keys(object).length !== 0) { if (!object.preisprofil) { diff --git a/application/shared/konzert/kasse.ts b/application/shared/konzert/kasse.ts index 99219639..de527efd 100644 --- a/application/shared/konzert/kasse.ts +++ b/application/shared/konzert/kasse.ts @@ -64,10 +64,6 @@ export default class Kasse { "10000": undefined, }; endbestandGezaehltEUR = 0; - /* eslint-disable-next-line @typescript-eslint/no-explicit-any*/ - toJSON(): any { - return Object.assign({}, this); - } constructor(object?: RecursivePartial & { kassenfreigabeAm?: Date | string }>) { if (object && keys(object).length !== 0) { diff --git a/application/shared/konzert/konzert.ts b/application/shared/konzert/konzert.ts index 8c6f737c..bd23fe85 100644 --- a/application/shared/konzert/konzert.ts +++ b/application/shared/konzert/konzert.ts @@ -53,25 +53,6 @@ export default class Konzert extends Veranstaltung { unterkunft: Unterkunft; - toJSON(): object { - const result = {}; - Object.assign(result, this, { - agentur: this.agentur.toJSON(), - artist: this.artist.toJSON(), - eintrittspreise: this.eintrittspreise.toJSON(), - hotel: this.hotel.toJSON(), - kasse: this.kasse.toJSON(), - kopf: this.kopf.toJSON(), - kosten: this.kosten.toJSON(), - presse: this.presse.toJSON(), - staff: this.staff.toJSON(), - technik: this.technik.toJSON(), - unterkunft: this.unterkunft.toJSON(), - vertrag: this.vertrag.toJSON(), - }); - return result; - } - constructor(object?: RecursivePartial & { startDate: string | Date; endDate: string | Date }>) { super(object); if (object) { diff --git a/application/shared/konzert/unterkunft.ts b/application/shared/konzert/unterkunft.ts index bf249528..73879e77 100644 --- a/application/shared/konzert/unterkunft.ts +++ b/application/shared/konzert/unterkunft.ts @@ -18,11 +18,6 @@ export default class Unterkunft { anreiseDate: Date; abreiseDate: Date; - /* eslint-disable-next-line @typescript-eslint/no-explicit-any*/ - toJSON(): any { - return Object.assign({}, this); - } - constructor( object: | RecursivePartial< diff --git a/application/shared/konzert/vertrag.ts b/application/shared/konzert/vertrag.ts index cf6c25e3..34143930 100644 --- a/application/shared/konzert/vertrag.ts +++ b/application/shared/konzert/vertrag.ts @@ -20,11 +20,6 @@ export default class Vertrag { return ["Deutsch", "Englisch", "Regional"]; } - /* eslint-disable-next-line @typescript-eslint/no-explicit-any*/ - toJSON(): any { - return Object.assign({}, this); - } - constructor(object?: RecursivePartial) { if (object && keys(object).length !== 0) { Object.assign(this, object, { diff --git a/application/shared/mail/mailRule.ts b/application/shared/mail/mailRule.ts index d619f403..c7bfe37b 100644 --- a/application/shared/mail/mailRule.ts +++ b/application/shared/mail/mailRule.ts @@ -140,11 +140,6 @@ export default class MailRule { return new MailRule(object); } - /* eslint-disable-next-line @typescript-eslint/no-explicit-any*/ - toJSON(): any { - return Object.assign({}, this); - } - constructor(object?: Partial) { if (object) { Object.assign(this, object); diff --git a/application/shared/optionen/ferienIcals.ts b/application/shared/optionen/ferienIcals.ts index 683bd657..8e55e501 100644 --- a/application/shared/optionen/ferienIcals.ts +++ b/application/shared/optionen/ferienIcals.ts @@ -1,7 +1,6 @@ import Termin, { TerminType } from "./termin.js"; import Misc from "../commons/misc.js"; import map from "lodash/map.js"; -import invokeMap from "lodash/invokeMap.js"; export class Ical { name = ""; @@ -14,11 +13,6 @@ export class Ical { } } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - toJSON(): any { - return Object.assign({}, this); - } - get color(): string { return Termin.colorForType(this.typ); } @@ -36,11 +30,6 @@ export class KalenderEvents { this.updatedAt = Misc.stringOrDateToDate(object.updatedAt); } } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - toJSON(): any { - return Object.assign({}, this); - } } export default class FerienIcals { @@ -52,11 +41,4 @@ export default class FerienIcals { this.icals = map(object.icals, (each) => new Ical(each)); } } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - toJSON(): any { - return Object.assign({}, this, { - icals: invokeMap(this.icals, "toJSON"), - }); - } } diff --git a/application/shared/optionen/optionValues.ts b/application/shared/optionen/optionValues.ts index 2c0db45f..7e4b491a 100644 --- a/application/shared/optionen/optionValues.ts +++ b/application/shared/optionen/optionValues.ts @@ -103,10 +103,6 @@ export default class OptionValues { return new OptionValues(object); } - toJSON(): object { - return Object.assign({}, this); - } - constructor(object?: Partial) { if (object) { Object.assign(this, object, { diff --git a/application/shared/optionen/orte.ts b/application/shared/optionen/orte.ts index 2c90c9ae..84c9f929 100644 --- a/application/shared/optionen/orte.ts +++ b/application/shared/optionen/orte.ts @@ -6,7 +6,6 @@ import toLower from "lodash/fp/toLower.js"; import prop from "lodash/fp/prop.js"; import { RecursivePartial } from "../commons/advancedTypes.js"; import map from "lodash/map.js"; -import invokeMap from "lodash/invokeMap.js"; const sortByNameCaseInsensitive = sortBy(flowRight(toLower, prop("name"))); @@ -22,22 +21,12 @@ export class Ort { Object.assign(this, object); } } - - toJSON(): object { - return Object.assign({}, this); - } } export default class Orte { id = "orte"; orte: Ort[] = []; - toJSON(): object { - return Object.assign({}, this, { - orte: invokeMap(this.orte, "toJSON"), - }); - } - constructor(object?: RecursivePartial) { if (object && object.orte) { this.orte = sortByNameCaseInsensitive(map(object.orte, (o: any) => new Ort(o))); diff --git a/application/shared/optionen/termin.ts b/application/shared/optionen/termin.ts index 5b3dea81..f8e72142 100644 --- a/application/shared/optionen/termin.ts +++ b/application/shared/optionen/termin.ts @@ -41,11 +41,6 @@ export default class Termin { this.id = object?.id ? object.id : encodeURIComponent(DatumUhrzeit.forJSDate(this.startDate).fuerCalendarWidget + this.beschreibung); } - /* eslint-disable-next-line @typescript-eslint/no-explicit-any*/ - toJSON(): any { - return Object.assign({}, this); - } - static colorForType(typ: TerminType): string { return { Sonstiges: "#d6bdff", diff --git a/application/shared/rider/rider.ts b/application/shared/rider/rider.ts index 511a024f..77f46bae 100644 --- a/application/shared/rider/rider.ts +++ b/application/shared/rider/rider.ts @@ -19,10 +19,6 @@ export class Rider { boxes: object?.boxes || [], }); } - - toJSON(): object { - return Object.assign({}, this); - } } export class PrintableBox implements BoxParams { diff --git a/application/shared/test/konzert/eintrittspreise_object.test.ts b/application/shared/test/konzert/eintrittspreise_object.test.ts index b8be42b2..bd18dd41 100644 --- a/application/shared/test/konzert/eintrittspreise_object.test.ts +++ b/application/shared/test/konzert/eintrittspreise_object.test.ts @@ -10,11 +10,11 @@ describe("Eintrittspreise", () => { describe("Initialisiert", () => { it("mit freiem Eintritt für leeres Objekt", () => { const preise = new Eintrittspreise({}); - expect(preise.toJSON()).to.eql(freierEintritt); + expect(preise).to.eql(freierEintritt); }); it("mit freiem Eintritt für 'undefined'", () => { const preise = new Eintrittspreise(); - expect(preise.toJSON()).to.eql(freierEintritt); + expect(preise).to.eql(freierEintritt); }); }); @@ -22,23 +22,23 @@ describe("Eintrittspreise", () => { it("fuer freien Eintritt", () => { const alterDatensatz = { frei: true, erwarteteBesucher: NaN }; const preise = new Eintrittspreise(alterDatensatz); - expect(preise.toJSON()).to.eql(freierEintritt); - expect(preise.toJSON().zuschuss).to.eql(0); - expect(preise.toJSON().erwarteteBesucher).to.eql(0); + expect(preise).to.eql(freierEintritt); + expect(preise.zuschuss).to.eql(0); + expect(preise.erwarteteBesucher).to.eql(0); }); it("fuer freien Eintritt mit gesetzten Rabatten", () => { const alterDatensatz = { frei: true, regulaer: 0, rabattErmaessigt: 2, rabattMitglied: 5, erwarteteBesucher: 0 }; const preise = new Eintrittspreise(alterDatensatz); - expect(preise.toJSON()).to.eql(freierEintritt); + expect(preise).to.eql(freierEintritt); }); it("nimmt alte Preise in neues custom Profil", () => { const alterDatensatz = { regulaer: 10, rabattErmaessigt: 2, rabattMitglied: 5, erwarteteBesucher: 50, zuschuss: 10 }; const preise = new Eintrittspreise(alterDatensatz); - expect(preise.toJSON().preisprofil).to.eql({ name: "Individuell (Alt)", rabattErmaessigt: 2, rabattMitglied: 5, regulaer: 10 }); - expect(preise.toJSON().zuschuss).to.eql(10); - expect(preise.toJSON().erwarteteBesucher).to.eql(50); + expect(preise.preisprofil).to.eql({ name: "Individuell (Alt)", rabattErmaessigt: 2, rabattMitglied: 5, regulaer: 10 }); + expect(preise.zuschuss).to.eql(10); + expect(preise.erwarteteBesucher).to.eql(50); }); }); @@ -46,13 +46,13 @@ describe("Eintrittspreise", () => { it("fuer freien Eintritt", () => { const alterDatensatz = { frei: true }; const preise = new Eintrittspreise(alterDatensatz); - expect(preise.toJSON()).to.eql(freierEintritt); + expect(preise).to.eql(freierEintritt); }); it("nimmt alte Preise in neues custom Profil", () => { const p = { preisprofil: { name: "18,00", regulaer: 18, rabattErmaessigt: 2, rabattMitglied: 5 } }; const preise = new Eintrittspreise(p); - expect(preise.toJSON().preisprofil).to.eql({ name: "18,00", regulaer: 18, rabattErmaessigt: 2, rabattMitglied: 5 }); + expect(preise.preisprofil).to.eql({ name: "18,00", regulaer: 18, rabattErmaessigt: 2, rabattMitglied: 5 }); }); }); }); diff --git a/application/shared/user/user.ts b/application/shared/user/user.ts index 57be99e8..927d235b 100644 --- a/application/shared/user/user.ts +++ b/application/shared/user/user.ts @@ -43,15 +43,9 @@ export default class User { }); } - /* eslint-disable-next-line @typescript-eslint/no-explicit-any*/ - toJSON(): any { - const result = Object.assign({}, this); - delete result.accessrightsTransient; - return result; - } - get withoutPass() { const result = cloneDeep(this); + delete result.accessrightsTransient; delete result.hashedPassword; delete result.salt; return result; @@ -105,10 +99,7 @@ export default class User { } get accessrights(): Accessrights { - if (!this.accessrightsTransient) { - this.accessrightsTransient = new Accessrights(this); - } - return this.accessrightsTransient; + return new Accessrights(this); } get asUserAsOption() { diff --git a/application/shared/veranstaltung/artist.ts b/application/shared/veranstaltung/artist.ts index 10c4ac65..260132af 100644 --- a/application/shared/veranstaltung/artist.ts +++ b/application/shared/veranstaltung/artist.ts @@ -15,10 +15,6 @@ export default class Artist { getInForMasterDate?: Date; bandTransport?: BandTransport; - toJSON(): object { - return Object.assign({}, this); - } - constructor(object?: Omit & { getInForMasterDate: string | Date }) { if (object && keys(object).length !== 0) { const getIn = Misc.stringOrDateToDate(object.getInForMasterDate); diff --git a/application/shared/veranstaltung/kontakt.ts b/application/shared/veranstaltung/kontakt.ts index 3de338e6..8a315362 100644 --- a/application/shared/veranstaltung/kontakt.ts +++ b/application/shared/veranstaltung/kontakt.ts @@ -8,11 +8,6 @@ export default class Kontakt { name = ""; telefon = ""; - /* eslint-disable-next-line @typescript-eslint/no-explicit-any*/ - toJSON(): any { - return Object.assign({}, this); - } - constructor(object?: RecursivePartial) { if (object && keys(object).length) { Object.assign(this, object); diff --git a/application/shared/veranstaltung/kopf.ts b/application/shared/veranstaltung/kopf.ts index 2d55915f..13a2fcb1 100644 --- a/application/shared/veranstaltung/kopf.ts +++ b/application/shared/veranstaltung/kopf.ts @@ -20,11 +20,6 @@ export default class Kopf { kannAufHomePage = false; kannInSocialMedia = false; - /* eslint-disable-next-line @typescript-eslint/no-explicit-any*/ - toJSON(): any { - return Object.assign({}, this); - } - constructor(object?: RecursivePartial) { if (object && keys(object).length) { Object.assign(this, object); diff --git a/application/shared/veranstaltung/kosten.ts b/application/shared/veranstaltung/kosten.ts index f1619178..1265cd3c 100644 --- a/application/shared/veranstaltung/kosten.ts +++ b/application/shared/veranstaltung/kosten.ts @@ -50,11 +50,6 @@ export default class Kosten { tontechniker = 0; lichttechniker = 0; - /* eslint-disable-next-line @typescript-eslint/no-explicit-any*/ - toJSON(): any { - return Object.assign({}, this); - } - constructor(object?: RecursivePartial) { if (object && keys(object).length) { Object.assign(this, object); diff --git a/application/shared/veranstaltung/presse.ts b/application/shared/veranstaltung/presse.ts index bb6c93eb..9848888a 100644 --- a/application/shared/veranstaltung/presse.ts +++ b/application/shared/veranstaltung/presse.ts @@ -9,11 +9,6 @@ export default class Presse { checked = false; jazzclubURL = ""; - /* eslint-disable-next-line @typescript-eslint/no-explicit-any*/ - toJSON(): any { - return Object.assign({}, this); - } - constructor(object?: RecursivePartial) { if (object && keys(object).length) { Object.assign(this, object, { diff --git a/application/shared/veranstaltung/staff.ts b/application/shared/veranstaltung/staff.ts index e6e9ce70..8e027eed 100644 --- a/application/shared/veranstaltung/staff.ts +++ b/application/shared/veranstaltung/staff.ts @@ -22,10 +22,6 @@ export default class Staff { merchandiseNotNeeded = true; ersthelferNotNeeded = false; - toJSON(): object { - return Object.assign({}, this); - } - constructor(object?: RecursivePartial) { if (object && keys(object).length) { Object.assign(this, object, { diff --git a/application/shared/veranstaltung/technik.ts b/application/shared/veranstaltung/technik.ts index 7c23cb2e..e6778a00 100644 --- a/application/shared/veranstaltung/technik.ts +++ b/application/shared/veranstaltung/technik.ts @@ -10,11 +10,6 @@ export default class Technik { checked = false; fluegel = false; - /* eslint-disable-next-line @typescript-eslint/no-explicit-any*/ - toJSON(): any { - return Object.assign({}, this); - } - constructor(object?: RecursivePartial) { if (object && keys(object).length) { Object.assign(this, object, { diff --git a/application/shared/veranstaltung/veranstaltung.ts b/application/shared/veranstaltung/veranstaltung.ts index bc2be30f..fe15c555 100644 --- a/application/shared/veranstaltung/veranstaltung.ts +++ b/application/shared/veranstaltung/veranstaltung.ts @@ -56,8 +56,6 @@ export default abstract class Veranstaltung { } } - abstract toJSON(): object; - abstract get isVermietung(): boolean; get fullyQualifiedUrl(): string { diff --git a/application/shared/vermietung/angebot.ts b/application/shared/vermietung/angebot.ts index d47433c3..171c5407 100644 --- a/application/shared/vermietung/angebot.ts +++ b/application/shared/vermietung/angebot.ts @@ -31,11 +31,6 @@ export default class Angebot { freigabeAm?: Date; rechnungsnummer?: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - toJSON(): any { - return Object.assign({}, this); - } - constructor(object?: Omit & { freigabeAm?: Date | string }) { if (object && keys(object).length) { Object.assign(this, object, { diff --git a/application/shared/vermietung/vermietung.ts b/application/shared/vermietung/vermietung.ts index 885c7286..d0ed74b7 100644 --- a/application/shared/vermietung/vermietung.ts +++ b/application/shared/vermietung/vermietung.ts @@ -13,21 +13,6 @@ export default class Vermietung extends Veranstaltung { angebot = new Angebot(); vertragspartner = new Kontakt(); - toJSON(): object { - const result = {}; - Object.assign(result, this, { - artist: this.artist.toJSON(), - kopf: this.kopf.toJSON(), - kosten: this.kosten.toJSON(), - presse: this.presse.toJSON(), - staff: this.staff.toJSON(), - technik: this.technik.toJSON(), - angebot: this.angebot.toJSON(), - vertragspartner: this.vertragspartner.toJSON(), - }); - return result; - } - constructor(object?: RecursivePartial & { startDate: string | Date; endDate: string | Date }>) { super(object ?? { brauchtPresse: false }); if (object) { diff --git a/application/vue/src/components/mails/MailRules.tsx b/application/vue/src/components/mails/MailRules.tsx index e41b62d8..07b6bf4a 100644 --- a/application/vue/src/components/mails/MailRules.tsx +++ b/application/vue/src/components/mails/MailRules.tsx @@ -6,20 +6,15 @@ import { Col } from "antd"; import MailRule, { allMailrules, MailRuleUI } from "jc-shared/mail/mailRule"; import EditableTable from "@/widgets/EditableTable/EditableTable.tsx"; import { Columns } from "@/widgets/EditableTable/types.ts"; -import cloneDeep from "lodash/cloneDeep"; import JazzFormAndHeader from "@/components/content/JazzFormAndHeader.tsx"; import { useJazzMutation } from "@/commons/useJazzMutation.ts"; -import invokeMap from "lodash/invokeMap"; import { JazzRow } from "@/widgets/JazzRow.tsx"; class MailRulesWrapper { allRules: MailRule[] = []; constructor(rules?: MailRule[]) { - this.allRules = invokeMap(rules, "toJSON"); - } - toJSON(): object { - return cloneDeep(this); + this.allRules = rules ?? []; } } diff --git a/application/vue/src/components/options/TerminePage.tsx b/application/vue/src/components/options/TerminePage.tsx index 33901880..ecb397b3 100644 --- a/application/vue/src/components/options/TerminePage.tsx +++ b/application/vue/src/components/options/TerminePage.tsx @@ -6,7 +6,6 @@ import { Col } from "antd"; import Termin, { TerminType } from "jc-shared/optionen/termin"; import EditableTable from "@/widgets/EditableTable/EditableTable.tsx"; import { Columns } from "@/widgets/EditableTable/types.ts"; -import cloneDeep from "lodash/cloneDeep"; import JazzFormAndHeader from "@/components/content/JazzFormAndHeader.tsx"; import { useJazzMutation } from "@/commons/useJazzMutation.ts"; import map from "lodash/map"; @@ -23,9 +22,6 @@ class TermineWrapper { typ: termin.typ, })); } - toJSON(): object { - return cloneDeep(this); - } } function TerminePageInternal() { diff --git a/application/vue/src/rest/loader.ts b/application/vue/src/rest/loader.ts index 58dfbdfe..a947644e 100644 --- a/application/vue/src/rest/loader.ts +++ b/application/vue/src/rest/loader.ts @@ -122,12 +122,7 @@ export async function konzertWithRiderForUrl(url: string): Promise { } export async function saveVermietung(vermietung: Vermietung) { - const result = await post({ url: "/rest/vermietung", data: vermietung.toJSON(), resType: vermietung }); + const result = await post({ url: "/rest/vermietung", data: vermietung }); return new Vermietung(result); } @@ -217,20 +212,20 @@ export async function allUsers(): Promise { } export async function saveUser(user: User) { - const result = await post({ url: "/rest/user", data: user.toJSON(), resType: user }); + const result = await post({ url: "/rest/user", data: user }); return new User(result); } export async function deleteUser(user: User) { - return loeschen({ url: "/rest/user", data: user.toJSON() }); + return loeschen({ url: "/rest/user", data: user }); } export async function saveNewUser(user: User) { - return standardFetch({ method: "PUT", url: "/rest/user", data: user.toJSON() }); + return standardFetch({ method: "PUT", url: "/rest/user", data: user }); } export async function changePassword(user: User) { - const result = await post({ url: "/rest/user/changePassword", data: user.toJSON(), resType: user }); + const result = await post({ url: "/rest/user/changePassword", data: user }); return new User(result); } @@ -259,21 +254,21 @@ export async function saveRider(rider: Rider) { // Optionen & Termine export async function optionen(): Promise { const result = await get({ url: "/rest/optionen", resType: new OptionValues() }); - return result ? new OptionValues(result) : result; + return new OptionValues(result); } export async function saveOptionen(optionen: OptionValues) { - const result = await post({ url: "/rest/optionen", data: optionen.toJSON(), resType: optionen }); + const result = await post({ url: "/rest/optionen", data: optionen }); return new OptionValues(result); } export async function orte() { const result = await get({ url: "/rest/orte", resType: new Orte() }); - return result ? new Orte(result) : result; + return new Orte(result); } export async function saveOrte(orte: Orte) { - const result = await post({ url: "/rest/orte", data: orte.toJSON(), resType: orte }); + const result = await post({ url: "/rest/orte", data: orte }); return new Orte(result); } diff --git a/application/vue/src/widgets/Uploader.tsx b/application/vue/src/widgets/Uploader.tsx index 05f4e8d8..533df746 100644 --- a/application/vue/src/widgets/Uploader.tsx +++ b/application/vue/src/widgets/Uploader.tsx @@ -42,6 +42,7 @@ export default function Uploader({ name, typ, onlyImages = false }: UploaderPara try { const newVeranstaltung = await uploadFile(formData); setFileList([]); + // eslint-disable-next-line @typescript-eslint/no-explicit-any const strings = name.reduce((prev, curr) => (prev as any)[curr], newVeranstaltung); form.setFieldValue(name, strings); } catch { From e8de52569c89727c789baef0cea555b347b8e628 Mon Sep 17 00:00:00 2001 From: leider Date: Sat, 22 Mar 2025 16:38:04 +0100 Subject: [PATCH 03/13] minor adjustments for ImageOverview.tsx --- .../options/imageoverview/ImageOverview.tsx | 14 +++++--------- .../imageoverview/useCreateImagenamesSections.ts | 4 ++-- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/application/vue/src/components/options/imageoverview/ImageOverview.tsx b/application/vue/src/components/options/imageoverview/ImageOverview.tsx index 34841ee0..43217fc3 100644 --- a/application/vue/src/components/options/imageoverview/ImageOverview.tsx +++ b/application/vue/src/components/options/imageoverview/ImageOverview.tsx @@ -12,17 +12,15 @@ import { useJazzMutation } from "@/commons/useJazzMutation.ts"; import filter from "lodash/filter"; import { JazzRow } from "@/widgets/JazzRow.tsx"; +type ImageOverviewForm = { with: ImageOverviewRow[]; notFound: ImageOverviewRow[]; unused: ImageOverviewRow[] }; + function isChanged(v: ImageOverviewRow) { return v.newname !== v.image; } export default function ImageOverview() { useDirtyBlocker(false); - const [form] = Form.useForm<{ - with: ImageOverviewRow[]; - notFound: ImageOverviewRow[]; - unused: ImageOverviewRow[]; - }>(); + const [form] = Form.useForm(); const mutateImages = useJazzMutation({ saveFunction: saveImagenames, @@ -42,10 +40,8 @@ export default function ImageOverview() { }, [form, sections]); function saveForm() { - const formValues = form.getFieldsValue(true); - const changedRows: ImageOverviewRow[] = filter(formValues.with, isChanged) - .concat(filter(formValues.notFound, isChanged)) - .concat(filter(formValues.unused, isChanged)); + const sects: ImageOverviewForm = form.getFieldsValue(true); + const changedRows = filter(sects.with.concat(sects.notFound).concat(sects.unused), isChanged); form.validateFields().then(async () => { mutateImages.mutate(changedRows); }); diff --git a/application/vue/src/components/options/imageoverview/useCreateImagenamesSections.ts b/application/vue/src/components/options/imageoverview/useCreateImagenamesSections.ts index 5b16cde1..135f9c25 100644 --- a/application/vue/src/components/options/imageoverview/useCreateImagenamesSections.ts +++ b/application/vue/src/components/options/imageoverview/useCreateImagenamesSections.ts @@ -1,5 +1,5 @@ import { useCallback, useMemo } from "react"; -import Konzert, { ImageOverviewVeranstaltung } from "jc-shared/konzert/konzert.ts"; +import Konzert, { ImageOverviewRow, ImageOverviewVeranstaltung } from "jc-shared/konzert/konzert.ts"; import uniq from "lodash/uniq"; import flatMap from "lodash/flatMap"; import intersection from "lodash/intersection"; @@ -39,7 +39,7 @@ export function useCreateImagenamesSections() { ); const toImageOverviewRow = useCallback( - (im: string) => ({ image: im, newname: im, veranstaltungen: elementsWithImage(im) }), + (im: string) => ({ image: im, newname: im, veranstaltungen: elementsWithImage(im) }) as ImageOverviewRow, [elementsWithImage], ); From ab6ee87db2e6aff9b6cc2e781453cc29b3ab5041 Mon Sep 17 00:00:00 2001 From: leider Date: Sat, 22 Mar 2025 17:39:10 +0100 Subject: [PATCH 04/13] add some tests for kontakte --- application/shared/optionen/optionValues.ts | 19 ++++-- .../optionen/optionValue_kontakte.test.ts | 65 +++++++++++++++++++ .../src/components/konzert/KonzertComp.tsx | 4 +- 3 files changed, 79 insertions(+), 9 deletions(-) create mode 100644 application/shared/test/optionen/optionValue_kontakte.test.ts diff --git a/application/shared/optionen/optionValues.ts b/application/shared/optionen/optionValues.ts index 7e4b491a..4e5a9ff9 100644 --- a/application/shared/optionen/optionValues.ts +++ b/application/shared/optionen/optionValues.ts @@ -106,11 +106,11 @@ export default class OptionValues { constructor(object?: Partial) { if (object) { Object.assign(this, object, { - kooperationen: sortByNameCaseInsensitive(object.kooperationen || []), - genres: sortByNameCaseInsensitive(object.genres || []), - backlineJazzclub: sortByNameCaseInsensitive(object.backlineJazzclub || []), - backlineRockshop: sortByNameCaseInsensitive(object.backlineRockshop || []), - artists: sortByNameCaseInsensitive(object.artists || []), + kooperationen: sortByNameCaseInsensitive(object.kooperationen), + genres: sortByNameCaseInsensitive(object.genres), + backlineJazzclub: sortByNameCaseInsensitive(object.backlineJazzclub), + backlineRockshop: sortByNameCaseInsensitive(object.backlineRockshop), + artists: sortByNameCaseInsensitive(object.artists), preisprofile: (object.preisprofile || preisprofileInitial()).sort((a: Preisprofil, b: Preisprofil) => { if (a.regulaer === b.regulaer) { return a.name.toLocaleLowerCase().localeCompare(b.name.toLocaleLowerCase()); @@ -126,8 +126,13 @@ export default class OptionValues { } } - addOrUpdateKontakt(kontaktKey: "agenturen" | "hotels", kontakt: Kontakt, selection: string): void { - if (!(selection || "[temporär]").match(/\[temporär]/)) { + addOrUpdateKontakt(kontaktKey: "agenturen" | "hotels", kontakt: Kontakt, selection?: string): void { + if ("[temporär]" !== selection) { + if (!kontakt.name) { + // we do nothing if name not given + return; + } + const ourCollection = kontaktKey === "agenturen" ? this.agenturen : this.hotels; remove(ourCollection, (k) => k.name === kontakt.name); ourCollection.push(kontakt); diff --git a/application/shared/test/optionen/optionValue_kontakte.test.ts b/application/shared/test/optionen/optionValue_kontakte.test.ts new file mode 100644 index 00000000..9fe9deed --- /dev/null +++ b/application/shared/test/optionen/optionValue_kontakte.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it } from "vitest"; +import OptionValues from "../../optionen/optionValues.js"; +import Kontakt from "../../veranstaltung/kontakt.js"; + +describe("Agenturen in Optionen", () => { + it("is properly initialized", () => { + const emptyOptionen = new OptionValues(); + expect(emptyOptionen).to.eql({ + agenturen: [], + artists: [], + backlineJazzclub: [], + backlineRockshop: [], + genres: [], + hotelpreise: [], + hotels: [], + id: "instance", + kooperationen: [], + preisKlavierstimmer: 125, + preisprofile: [], + typenPlus: [], + }); + }); + + describe("addOrUpdateKontakt", () => { + it("adds Agentur if name is set", () => { + const emptyOptionen = new OptionValues(); + const kontakt = new Kontakt({ name: "Name" }); + emptyOptionen.addOrUpdateKontakt("agenturen", kontakt); + expect(emptyOptionen.agenturen).to.eql([{ adresse: "", ansprechpartner: "", email: "", name: "Name", telefon: "" }]); + }); + + it("changes existing Agentur if name is set", () => { + const optionenWithAgentur = new OptionValues({ agenturen: [new Kontakt({ name: "Name" })] }); + expect(optionenWithAgentur.agenturen).toHaveLength(1); + const kontakt = new Kontakt({ name: "Name", telefon: "123" }); + optionenWithAgentur.addOrUpdateKontakt("agenturen", kontakt); + expect(optionenWithAgentur.agenturen).to.eql([{ adresse: "", ansprechpartner: "", email: "", name: "Name", telefon: "123" }]); + }); + + it("adds second Agentur if name is not yet in", () => { + const optionenWithAgentur = new OptionValues({ agenturen: [new Kontakt({ name: "Name" })] }); + expect(optionenWithAgentur.agenturen).toHaveLength(1); + const kontakt = new Kontakt({ name: "Name2" }); + optionenWithAgentur.addOrUpdateKontakt("agenturen", kontakt); + expect(optionenWithAgentur.agenturen).to.eql([ + { adresse: "", ansprechpartner: "", email: "", name: "Name", telefon: "" }, + { adresse: "", ansprechpartner: "", email: "", name: "Name2", telefon: "" }, + ]); + }); + + it("does not add an Agentur without name", () => { + const emptyOptionen = new OptionValues(); + const kontakt = new Kontakt({ name: "" }); + emptyOptionen.addOrUpdateKontakt("agenturen", kontakt); + expect(emptyOptionen.agenturen).to.be.empty; + }); + + it('does not add an Agentur if "[temporär]"', () => { + const emptyOptionen = new OptionValues(); + const kontakt = new Kontakt({ name: "Name" }); + emptyOptionen.addOrUpdateKontakt("agenturen", kontakt, "[temporär]"); + expect(emptyOptionen.agenturen).to.be.empty; + }); + }); +}); diff --git a/application/vue/src/components/konzert/KonzertComp.tsx b/application/vue/src/components/konzert/KonzertComp.tsx index 826634e8..0ee16153 100644 --- a/application/vue/src/components/konzert/KonzertComp.tsx +++ b/application/vue/src/components/konzert/KonzertComp.tsx @@ -56,10 +56,10 @@ export default function KonzertComp() { } const untypedKonzert = vals as { agenturauswahl?: string; hotelauswahl?: string; hotelpreiseAlsDefault?: boolean }; - optionen.addOrUpdateKontakt("agenturen", konz.agentur, untypedKonzert.agenturauswahl ?? ""); + optionen.addOrUpdateKontakt("agenturen", konz.agentur, untypedKonzert.agenturauswahl); delete untypedKonzert.agenturauswahl; if (konz.artist.brauchtHotel) { - optionen.addOrUpdateKontakt("hotels", konz.hotel, untypedKonzert.hotelauswahl ?? ""); + optionen.addOrUpdateKontakt("hotels", konz.hotel, untypedKonzert.hotelauswahl); delete untypedKonzert.hotelauswahl; if (untypedKonzert.hotelpreiseAlsDefault) { optionen.updateHotelpreise(konz.hotel, konz.unterkunft.zimmerPreise); From c144127256274f2bde265f1e26a5e72d19c4beae Mon Sep 17 00:00:00 2001 From: leider Date: Sat, 22 Mar 2025 18:07:09 +0100 Subject: [PATCH 05/13] introduce many sortBy instead of sort --- application/backend/lib/konzerte/imageService.ts | 3 ++- application/shared/optionen/optionValues.ts | 6 ++---- application/shared/programmheft/kalender.ts | 6 +++--- .../vue/src/components/konzert/preview/GaesteInPreview.tsx | 3 ++- .../options/imageoverview/useCreateImagenamesSections.ts | 3 ++- .../vue/src/components/programmheft/Programmheft.tsx | 6 +++--- application/vue/src/widgets/PreisprofilSelect.tsx | 3 ++- 7 files changed, 16 insertions(+), 14 deletions(-) diff --git a/application/backend/lib/konzerte/imageService.ts b/application/backend/lib/konzerte/imageService.ts index ae9006d5..0a5e2376 100644 --- a/application/backend/lib/konzerte/imageService.ts +++ b/application/backend/lib/konzerte/imageService.ts @@ -4,6 +4,7 @@ import { ImageOverviewRow } from "jc-shared/konzert/konzert.js"; import store from "./konzertestore.js"; import conf from "jc-shared/commons/simpleConfigure.js"; import map from "lodash/map.js"; +import sortBy from "lodash/sortBy.js"; async function renameImage(oldname: string, newname: string, konzertIds: string[], user: User) { function updateKonzert(id: string) { @@ -31,6 +32,6 @@ export default { alleBildNamen: async function alleBildNamen() { const files = await fs.readdir(conf.uploadDir); - return files.sort(); + return sortBy(files); }, }; diff --git a/application/shared/optionen/optionValues.ts b/application/shared/optionen/optionValues.ts index 4e5a9ff9..7dda4176 100644 --- a/application/shared/optionen/optionValues.ts +++ b/application/shared/optionen/optionValues.ts @@ -111,15 +111,13 @@ export default class OptionValues { backlineJazzclub: sortByNameCaseInsensitive(object.backlineJazzclub), backlineRockshop: sortByNameCaseInsensitive(object.backlineRockshop), artists: sortByNameCaseInsensitive(object.artists), - preisprofile: (object.preisprofile || preisprofileInitial()).sort((a: Preisprofil, b: Preisprofil) => { + preisprofile: (object.preisprofile ?? preisprofileInitial()).sort((a, b) => { if (a.regulaer === b.regulaer) { return a.name.toLocaleLowerCase().localeCompare(b.name.toLocaleLowerCase()); } return a.regulaer > b.regulaer ? 1 : -1; }), - typenPlus: (object.typenPlus ?? []).sort((a: TypMitMehr, b: TypMitMehr) => - a.name.toLocaleLowerCase().localeCompare(b.name.toLocaleLowerCase()), - ), + typenPlus: (object.typenPlus ?? []).sort((a, b) => a.name.toLocaleLowerCase().localeCompare(b.name.toLocaleLowerCase())), agenturen: sortKontakte(object.agenturen), hotels: sortKontakte(object.hotels), }); diff --git a/application/shared/programmheft/kalender.ts b/application/shared/programmheft/kalender.ts index 7e69c5a5..02b7426f 100644 --- a/application/shared/programmheft/kalender.ts +++ b/application/shared/programmheft/kalender.ts @@ -4,6 +4,7 @@ import { Event } from "./Event.js"; import { RecursivePartial } from "../commons/advancedTypes.js"; import map from "lodash/map.js"; import invokeMap from "lodash/invokeMap.js"; +import sortBy from "lodash/sortBy.js"; export default class Kalender { id: string; @@ -31,9 +32,8 @@ export default class Kalender { const thisDatum = DatumUhrzeit.forYYYYslashMM(this.id); const otherDatum = DatumUhrzeit.forYYYYslashMM(otherKalId); const differenz = otherDatum.differenzInMonaten(thisDatum); - const result = invokeMap(this.events, "cloneAndMoveBy", { monate: differenz }); - result.sort((a, b) => a.start.localeCompare(b.start)); - return result; + const result: Event[] = invokeMap(this.events, "cloneAndMoveBy", { monate: differenz }); + return sortBy(result, ["start"]); } sortEvents() { diff --git a/application/vue/src/components/konzert/preview/GaesteInPreview.tsx b/application/vue/src/components/konzert/preview/GaesteInPreview.tsx index 3ae41367..1086c345 100644 --- a/application/vue/src/components/konzert/preview/GaesteInPreview.tsx +++ b/application/vue/src/components/konzert/preview/GaesteInPreview.tsx @@ -9,6 +9,7 @@ import { colorsAndIconsForSections } from "@/widgets/buttonsAndIcons/colorsIcons import { useJazzContext } from "@/components/content/useJazzContext.ts"; import ButtonWithIconAndLink from "@/widgets/buttonsAndIcons/ButtonWithIconAndLink.tsx"; import { JazzRow } from "@/widgets/JazzRow"; +import sortBy from "lodash/sortBy"; function AddOrRemoveGastButton({ konzert, @@ -42,7 +43,7 @@ function AddOrRemoveGastButton({ } function GastResList({ source, art, konzert }: { readonly konzert: Konzert; readonly source: NameWithNumber[]; readonly art: GastArt }) { - const dataSource = useMemo(() => source.sort((a, b) => a.name.localeCompare(b.name)), [source]); + const dataSource = useMemo(() => sortBy(source, "name"), [source]); return ( { const convertString = (a: string): string => a.replace(/\s/g, "_"); - const imagenamesOfVeranstaltungen = uniq(flatMap(veranstaltungen, "images")).sort() as string[]; + const imagenamesOfVeranstaltungen = sortBy(uniq(flatMap(veranstaltungen, "images") as string[])); return { with: map(intersection(imagenames, imagenamesOfVeranstaltungen), toImageOverviewRow), diff --git a/application/vue/src/components/programmheft/Programmheft.tsx b/application/vue/src/components/programmheft/Programmheft.tsx index 19106db9..c1457474 100644 --- a/application/vue/src/components/programmheft/Programmheft.tsx +++ b/application/vue/src/components/programmheft/Programmheft.tsx @@ -23,6 +23,7 @@ import useFormInstance from "antd/es/form/hooks/useFormInstance"; import { useJazzMutation } from "@/commons/useJazzMutation.ts"; import map from "lodash/map"; import invokeMap from "lodash/invokeMap"; +import sortBy from "lodash/sortBy"; function ProgrammheftInternal({ start }: { readonly start: DatumUhrzeit }) { const form = useFormInstance(); @@ -38,9 +39,8 @@ function ProgrammheftInternal({ start }: { readonly start: DatumUhrzeit }) { const moveEvents = useCallback( (offset: number) => { function moveEventsBy(events: Event[], options: AdditionOptions) { - const result = invokeMap(events, "cloneAndMoveBy", options); - result.sort((a, b) => a.start.localeCompare(b.start)); - return result; + const result: Event[] = invokeMap(events, "cloneAndMoveBy", options); + return sortBy(result, "start"); } const newEvents = moveEventsBy(events, { tage: offset }); diff --git a/application/vue/src/widgets/PreisprofilSelect.tsx b/application/vue/src/widgets/PreisprofilSelect.tsx index da046061..f607272c 100644 --- a/application/vue/src/widgets/PreisprofilSelect.tsx +++ b/application/vue/src/widgets/PreisprofilSelect.tsx @@ -4,6 +4,7 @@ import OptionValues, { Preisprofil } from "jc-shared/optionen/optionValues"; import useFormInstance from "antd/es/form/hooks/useFormInstance"; import find from "lodash/find"; import map from "lodash/map"; +import sortBy from "lodash/sortBy"; interface PreisprofilSelectParams { readonly optionen: OptionValues; @@ -54,7 +55,7 @@ function InternalPreisprofilSelect({ id, onValueAsObject, optionen, valueAsObjec if (valueAsObject && !find(optionen.preisprofile, { name: valueAsObject.name })) { result.push({ ...valueAsObject, veraltet: true }); } - return result.sort((a, b) => (a.regulaer > b.regulaer ? 1 : -1)); + return sortBy(result, "regulaer"); }, [optionen, valueAsObject]); const displayProfile = useMemo(() => { From e886a98ed702cdc82a48d5445467f59f85295587 Mon Sep 17 00:00:00 2001 From: leider Date: Sun, 23 Mar 2025 10:34:15 +0100 Subject: [PATCH 06/13] code style --- application/shared/user/user.ts | 3 -- .../options/imageoverview/ImageOverview.tsx | 6 +--- application/vue/src/rest/loader.ts | 30 ++++--------------- 3 files changed, 7 insertions(+), 32 deletions(-) diff --git a/application/shared/user/user.ts b/application/shared/user/user.ts index 927d235b..8f505125 100644 --- a/application/shared/user/user.ts +++ b/application/shared/user/user.ts @@ -26,7 +26,6 @@ export default class User { mailinglisten: string[] = []; wantsEmailReminders?: boolean; password?: string; // take care to not persist! - accessrightsTransient?: Accessrights; // transient kannKasse?: boolean; kannTon?: boolean; @@ -35,7 +34,6 @@ export default class User { kannErsthelfer?: boolean; constructor(object: Partial) { - delete this.accessrightsTransient; this.id = object.id!; Object.assign(this, object, { kassenfreigabe: object.kassenfreigabe || object.rechte?.includes("kassenfreigabe"), @@ -45,7 +43,6 @@ export default class User { get withoutPass() { const result = cloneDeep(this); - delete result.accessrightsTransient; delete result.hashedPassword; delete result.salt; return result; diff --git a/application/vue/src/components/options/imageoverview/ImageOverview.tsx b/application/vue/src/components/options/imageoverview/ImageOverview.tsx index 43217fc3..259aff60 100644 --- a/application/vue/src/components/options/imageoverview/ImageOverview.tsx +++ b/application/vue/src/components/options/imageoverview/ImageOverview.tsx @@ -14,10 +14,6 @@ import { JazzRow } from "@/widgets/JazzRow.tsx"; type ImageOverviewForm = { with: ImageOverviewRow[]; notFound: ImageOverviewRow[]; unused: ImageOverviewRow[] }; -function isChanged(v: ImageOverviewRow) { - return v.newname !== v.image; -} - export default function ImageOverview() { useDirtyBlocker(false); const [form] = Form.useForm(); @@ -41,7 +37,7 @@ export default function ImageOverview() { function saveForm() { const sects: ImageOverviewForm = form.getFieldsValue(true); - const changedRows = filter(sects.with.concat(sects.notFound).concat(sects.unused), isChanged); + const changedRows = filter(sects.with.concat(sects.notFound).concat(sects.unused), (v) => v.newname !== v.image); form.validateFields().then(async () => { mutateImages.mutate(changedRows); }); diff --git a/application/vue/src/rest/loader.ts b/application/vue/src/rest/loader.ts index a947644e..fb97815d 100644 --- a/application/vue/src/rest/loader.ts +++ b/application/vue/src/rest/loader.ts @@ -73,12 +73,7 @@ export async function uploadFile(data: FormData) { } export async function uploadWikiImage(data: FormData) { - return await post({ - url: "/rest/wiki/upload", - data, - - resType: { url: "" }, - }); + return await post({ url: "/rest/wiki/upload", data, resType: { url: "" } }); } function handleVeranstaltungen(result?: Konzert[]): Konzert[] { @@ -127,29 +122,19 @@ export async function saveKonzert(konzert: Konzert) { } export async function deleteKonzertWithId(id: string) { - return loeschen({ - url: "/rest/konzert", - data: { id }, - }); + return loeschen({ url: "/rest/konzert", data: { id } }); } // Staff export async function addOrRemoveUserToSection(veranstaltung: Veranstaltung, section: StaffType, add: boolean) { - const result = await post({ - url: `/rest${veranstaltung.fullyQualifiedUrl}/${add ? "addUserToSection" : "removeUserFromSection"}`, - data: { section }, - resType: veranstaltung, - }); + const action = add ? "addUserToSection" : "removeUserFromSection"; + const result = await post({ url: `/rest${veranstaltung.fullyQualifiedUrl}/${action}`, data: { section }, resType: veranstaltung }); return veranstaltung.isVermietung ? new Vermietung(result) : new Konzert(result); } // Gäste export async function updateGastInSection(konzert: Konzert, item: NameWithNumber, art: GastArt) { - const result = await post({ - url: `/rest${konzert.fullyQualifiedUrl}/updateGastInSection`, - data: { item, art }, - resType: konzert, - }); + const result = await post({ url: `/rest${konzert.fullyQualifiedUrl}/updateGastInSection`, data: { item, art }, resType: konzert }); return new Konzert(result); } @@ -352,10 +337,7 @@ export async function searchWiki(suchtext: string) { } export async function deleteWikiPage(subdir: string, page: string) { - return loeschen({ - url: `/rest/wikipage/${subdir}/${page}`, - data: { data: "" }, - }); + return loeschen({ url: `/rest/wikipage/${subdir}/${page}`, data: { data: "" } }); } // Calendar From ddc367ebd11ef6d8b2566ab0eb833b825e4acf88 Mon Sep 17 00:00:00 2001 From: leider Date: Sun, 23 Mar 2025 10:50:22 +0100 Subject: [PATCH 07/13] introduce "/rest" as prefix in loader.ts --- application/vue/src/app/JazzclubApp.tsx | 2 + application/vue/src/rest/loader.ts | 108 ++++++++++++------------ 2 files changed, 54 insertions(+), 56 deletions(-) diff --git a/application/vue/src/app/JazzclubApp.tsx b/application/vue/src/app/JazzclubApp.tsx index a3651a60..a97a9426 100644 --- a/application/vue/src/app/JazzclubApp.tsx +++ b/application/vue/src/app/JazzclubApp.tsx @@ -93,6 +93,8 @@ function JazzclubApp() { }} locale={locale_de} theme={{ + cssVar: true, + hashed: false, token: { colorPrimary: "#337ab7", colorTextDisabled, diff --git a/application/vue/src/rest/loader.ts b/application/vue/src/rest/loader.ts index fb97815d..ab807c45 100644 --- a/application/vue/src/rest/loader.ts +++ b/application/vue/src/rest/loader.ts @@ -26,6 +26,7 @@ import sortBy from "lodash/sortBy"; type ContentType = "pdf" | "zip" | "other"; type FetchParams = { + urlPrefix?: string; url: string; contentType?: ContentType; method: Method; @@ -45,7 +46,7 @@ async function post(params: Omit, "method">) { return standardFetch({ ...params, method: "POST" }); } -async function standardFetch(params: FetchParams) { +async function standardFetch({ urlPrefix = "/rest", url, method, data, contentType }: FetchParams) { if (!axios.defaults.headers.Authorization) { await refreshTokenPost(); } else { @@ -57,23 +58,18 @@ async function standardFetch(params: FetchParams) { console.log("token veraltet"); } } - const options: AxiosRequestConfig = { - url: params.url, - method: params.method, - data: params.data, - responseType: params.contentType ? "blob" : "json", - }; + const options: AxiosRequestConfig = { url: urlPrefix + url, method: method, data: data, responseType: contentType ? "blob" : "json" }; const res = await axios>(options); return res.data; } export async function uploadFile(data: FormData) { - const result = await post({ url: "/rest/upload", data, resType: new Konzert() }); + const result = await post({ url: "/upload", data, resType: new Konzert() }); return new Konzert(result); } export async function uploadWikiImage(data: FormData) { - return await post({ url: "/rest/wiki/upload", data, resType: { url: "" } }); + return await post({ url: "/wiki/upload", data, resType: { url: "" } }); } function handleVeranstaltungen(result?: Konzert[]): Konzert[] { @@ -81,17 +77,17 @@ function handleVeranstaltungen(result?: Konzert[]): Konzert[] { } export async function konzerteBetweenYYYYMM(start: string, end: string) { - const result = await get({ url: `/rest/konzerte/${start}/${end}`, resType: [new Konzert()] }); + const result = await get({ url: `/konzerte/${start}/${end}`, resType: [new Konzert()] }); return handleVeranstaltungen(result); } export async function konzerteForToday() { - const result = await get({ url: `/rest/konzerte/fortoday`, resType: [new Konzert()] }); + const result = await get({ url: `/konzerte/fortoday`, resType: [new Konzert()] }); return handleVeranstaltungen(result); } export async function konzerteForTeam(selector: "zukuenftige" | "vergangene" | "alle") { - const result = await get({ url: `/rest/konzerte/${selector}`, resType: [new Konzert()] }); + const result = await get({ url: `/konzerte/${selector}`, resType: [new Konzert()] }); return handleVeranstaltungen(result); } @@ -102,7 +98,7 @@ export async function konzertWithRiderForUrl(url: string): Promise { } if (url.startsWith("copy-of-")) { const realUrl = url.substring(8); - const result = await get({ url: `/rest/vermietung/${encodeURIComponent(realUrl)}`, resType: new Vermietung() }); + const result = await get({ url: `/vermietung/${encodeURIComponent(realUrl)}`, resType: new Vermietung() }); if (result) { const vermietung = new Vermietung(result); vermietung.reset(); @@ -168,23 +164,23 @@ export async function vermietungForUrl(url: string): Promise { return result; } } - const result = await get({ url: `/rest/vermietung/${encodeURIComponent(url)}`, resType: new Vermietung() }); + const result = await get({ url: `/vermietung/${encodeURIComponent(url)}`, resType: new Vermietung() }); return result ? new Vermietung(result) : result; } export async function saveVermietung(vermietung: Vermietung) { - const result = await post({ url: "/rest/vermietung", data: vermietung }); + const result = await post({ url: "/vermietung", data: vermietung }); return new Vermietung(result); } export async function deleteVermietungWithId(id: string) { - return loeschen({ url: "/rest/vermietung", data: { id } }); + return loeschen({ url: "/vermietung", data: { id } }); } // User export async function currentUser() { try { - const result = await get({ url: "/rest/users/current", resType: {} as User }); + const result = await get({ url: "/users/current", resType: {} as User }); return new User(result); } catch { return new User({ id: "invalidUser" }); @@ -192,152 +188,152 @@ export async function currentUser() { } export async function allUsers(): Promise { - const result = await get({ url: "/rest/users", resType: [{} as User] }); + const result = await get({ url: "/users", resType: [{} as User] }); return map(result, (user) => new User(user)); } export async function saveUser(user: User) { - const result = await post({ url: "/rest/user", data: user }); + const result = await post({ url: "/user", data: user }); return new User(result); } export async function deleteUser(user: User) { - return loeschen({ url: "/rest/user", data: user }); + return loeschen({ url: "/user", data: user }); } export async function saveNewUser(user: User) { - return standardFetch({ method: "PUT", url: "/rest/user", data: user }); + return standardFetch({ method: "PUT", url: "/user", data: user }); } export async function changePassword(user: User) { - const result = await post({ url: "/rest/user/changePassword", data: user }); + const result = await post({ url: "/user/changePassword", data: user }); return new User(result); } // Programmheft export async function kalenderFor(jahrMonat: string) { - const result = await get({ url: `/rest/programmheft/${jahrMonat}`, resType: new Kalender() }); + const result = await get({ url: `/programmheft/${jahrMonat}`, resType: new Kalender() }); return result?.id ? new Kalender(result) : new Kalender({ id: jahrMonat }); } export async function alleKalender() { - const result = await get({ url: "/rest/programmheft/alle", resType: [new Kalender()] }); + const result = await get({ url: "/programmheft/alle", resType: [new Kalender()] }); return result.length > 0 ? map(result, (r) => new Kalender(r)) : []; } export async function saveProgrammheft(kalender: Kalender) { - const result = await post({ url: "/rest/programmheft", data: kalender }); + const result = await post({ url: "/programmheft", data: kalender }); return new Kalender(result); } // Rider export async function saveRider(rider: Rider) { - const result = await post({ url: "/rest/riders", data: rider }); + const result = await post({ url: "/riders", data: rider }); return new Rider(result); } // Optionen & Termine export async function optionen(): Promise { - const result = await get({ url: "/rest/optionen", resType: new OptionValues() }); + const result = await get({ url: "/optionen", resType: new OptionValues() }); return new OptionValues(result); } export async function saveOptionen(optionen: OptionValues) { - const result = await post({ url: "/rest/optionen", data: optionen }); + const result = await post({ url: "/optionen", data: optionen }); return new OptionValues(result); } export async function orte() { - const result = await get({ url: "/rest/orte", resType: new Orte() }); + const result = await get({ url: "/orte", resType: new Orte() }); return new Orte(result); } export async function saveOrte(orte: Orte) { - const result = await post({ url: "/rest/orte", data: orte }); + const result = await post({ url: "/orte", data: orte }); return new Orte(result); } export async function termine() { - const result = await get({ url: "/rest/termine", resType: [new Termin()] }); + const result = await get({ url: "/termine", resType: [new Termin()] }); return map(result, (r) => new Termin(r)) ?? []; } export async function saveTermine(termine: Termin[]) { - const result = await post({ url: "/rest/termine", data: termine }); + const result = await post({ url: "/termine", data: termine }); return map(result, (r) => new Termin(r)) ?? []; } export async function kalender() { - const result = await get({ url: "/rest/kalender", resType: new FerienIcals() }); + const result = await get({ url: "/kalender", resType: new FerienIcals() }); return result ? new FerienIcals(result) : result; } export async function saveKalender(kalender: FerienIcals) { - const result = await post({ url: "/rest/kalender", data: kalender }); + const result = await post({ url: "/kalender", data: kalender }); return result ? new FerienIcals(result) : result; } // Image export async function imagenames() { - const result = await get({ url: "/rest/imagenames", resType: { names: [""] } }); + const result = await get({ url: "/imagenames", resType: { names: [""] } }); return result?.names ?? []; } export async function saveImagenames(rows: ImageOverviewRow[]) { - await post({ url: "/rest/imagenames", data: rows, resType: { names: [""] } }); + await post({ url: "/imagenames", data: rows, resType: { names: [""] } }); return rows; } //Mails intern export async function sendMail(formData: FormData) { - return post({ url: "/rest/rundmail", data: formData, resType: {} as SentMessageInfo }); + return post({ url: "/rundmail", data: formData, resType: {} as SentMessageInfo }); } export async function allMailinglists() { - const result = await get({ url: "/rest/mailinglisten", resType: [] as User[] }); + const result = await get({ url: "/mailinglisten", resType: [] as User[] }); return { lists: sortBy(new Users(result ?? []).mailinglisten, "name") }; } export async function saveMailinglists({ lists }: { lists: Mailingliste[] }) { - const result = await post({ url: "/rest/mailinglisten", data: lists, resType: [] as User[] }); + const result = await post({ url: "/mailinglisten", data: lists, resType: [] as User[] }); return { lists: sortBy(new Users(result ?? []).mailinglisten, "name") }; } // Mails für Veranstaltungen export async function mailRules() { - const result = await get({ url: "/rest/mailrule", resType: [new MailRule()] }); + const result = await get({ url: "/mailrule", resType: [new MailRule()] }); return map(result, (each) => new MailRule(each)); } export async function saveMailRules(rules: MailRule[]) { - return post({ url: "/rest/mailrules", data: rules }); + return post({ url: "/mailrules", data: rules }); } // Wiki export async function wikisubdirs() { - const json = await get({ url: "/rest/wikidirs", resType: { dirs: [""] } }); + const json = await get({ url: "/wikidirs", resType: { dirs: [""] } }); return json ?? { dirs: [] }; } export async function wikiPage(subdir: string, page: string) { - const result = await get({ url: `/rest/wikipage/${subdir}/${page}`, resType: { content: "" } }); + const result = await get({ url: `/wikipage/${subdir}/${page}`, resType: { content: "" } }); return result?.content ?? ""; } export async function saveWikiPage(subdir: string, page: string, data: { content: string }) { - return post({ url: `/rest/wikipage/${subdir}/${page}`, data }); + return post({ url: `/wikipage/${subdir}/${page}`, data }); } export async function searchWiki(suchtext: string) { return post({ - url: "/rest/wikipage/search", + url: "/wikipage/search", data: { suchtext }, resType: { searchtext: "", matches: [{ pageName: "", line: "", text: "" }] }, }); } export async function deleteWikiPage(subdir: string, page: string) { - return loeschen({ url: `/rest/wikipage/${subdir}/${page}`, data: { data: "" } }); + return loeschen({ url: `/wikipage/${subdir}/${page}`, data: { data: "" } }); } // Calendar @@ -352,7 +348,7 @@ export async function calendarEventSources({ options?: TerminFilterOptions; isDarkMode: boolean; }) { - const segments = [`/rest/fullcalendarevents.json?start=${start.toISOString()}&end=${end.toISOString()}&darkMode=${isDarkMode}`]; + const segments = [`/fullcalendarevents.json?start=${start.toISOString()}&end=${end.toISOString()}&darkMode=${isDarkMode}`]; if (options) { segments.push(`&options=${JSON.stringify(options)}`); } @@ -361,12 +357,12 @@ export async function calendarEventSources({ // History export async function historyIdsFor(collection: string) { - const result = await get({ url: `/rest/history/${collection}` }); + const result = await get({ url: `/history/${collection}` }); return result as HistoryObjectOverview[]; } export async function historyRowsFor(collection: string, id: string) { - const result = await get({ url: `/rest/history/${collection}/${encodeURIComponent(id)}`, resType: [{} as HistoryDBType] }); + const result = await get({ url: `/history/${collection}/${encodeURIComponent(id)}`, resType: [{} as HistoryDBType] }); return historyFromRawRows(result); } From 7de0f8248f9717269754cc7af2fa90be1c5c5702 Mon Sep 17 00:00:00 2001 From: leider Date: Sun, 23 Mar 2025 12:28:15 +0100 Subject: [PATCH 08/13] add option to filter bookers in veranstaltung --- .../shared/veranstaltung/veranstaltung.ts | 3 +- .../components/team/TeamFilter/TeamFilter.tsx | 65 +++++++++---------- .../team/TeamFilter/TeamFilterEdit.tsx | 16 ++++- .../team/TeamFilter/applyTeamFilter.ts | 20 +++++- .../team/TeamFilter/resetTeamFilter.ts | 1 + .../components/team/applyTeamFilter.test.ts | 27 ++++++-- 6 files changed, 87 insertions(+), 45 deletions(-) diff --git a/application/shared/veranstaltung/veranstaltung.ts b/application/shared/veranstaltung/veranstaltung.ts index fe15c555..c6c13bfc 100644 --- a/application/shared/veranstaltung/veranstaltung.ts +++ b/application/shared/veranstaltung/veranstaltung.ts @@ -13,7 +13,6 @@ import { colorVermietung } from "../optionen/optionValues.js"; import { RecursivePartial } from "../commons/advancedTypes.js"; import map from "lodash/map.js"; import tinycolor from "tinycolor2"; -import User from "../user/user.js"; export type MinimalVeranstaltung = Partial & { id: string; startDate: Date; kopf: Kopf; url: string; ghost: boolean }; export default abstract class Veranstaltung { @@ -29,7 +28,7 @@ export default abstract class Veranstaltung { staff = new Staff(); technik = new Technik(); brauchtPresse = true; - booker?: User[] = []; + booker?: string[] = []; constructor( object?: RecursivePartial & { startDate: string | Date; endDate: string | Date }>, diff --git a/application/vue/src/components/team/TeamFilter/TeamFilter.tsx b/application/vue/src/components/team/TeamFilter/TeamFilter.tsx index 456d02d0..e72ab669 100644 --- a/application/vue/src/components/team/TeamFilter/TeamFilter.tsx +++ b/application/vue/src/components/team/TeamFilter/TeamFilter.tsx @@ -31,11 +31,30 @@ export default function TeamFilter() { form.setFieldsValue(filterObj); }, [filterObj, form]); + const closeBooleanTagForProp = useCallback( + (prop?: NamePath) => () => { + if (prop) { + form.setFieldValue(prop, undefined); + setFilter(form.getFieldsValue(true)); + } + }, + [form, setFilter], + ); + + const closePropTag = useCallback( + (label: string, prop?: NamePath) => () => { + const values = filter(form.getFieldValue(prop), (value: string) => value !== label); + form.setFieldValue(prop, values); + setFilter(form.getFieldsValue(true)); + }, + [form, setFilter], + ); + const eventTypTag = useCallback( (typ: string) => { const result = find(optionen.typenPlus, ["name", typ]); if (result) { - return { label: result.name, color: result.color }; + return { label: result.name, color: result.color, prop: ["kopf", "eventTyp"] }; } return undefined; }, @@ -44,38 +63,15 @@ export default function TeamFilter() { function headerTagsForFilters(labelsColors: LabelColorProperty[]) { function HeaderTag({ label, color, prop }: LabelColorProperty) { - if (isBoolean(color)) { - return ( - { - if (prop) { - form.setFieldValue(prop, undefined); - setFilter(form.getFieldsValue(true)); - } - }} - > - {label} - - ); - } else { - return ( - { - const typen = filter(form.getFieldValue(["kopf", "eventTyp"]), (typ: string) => typ !== label); - form.setFieldValue(["kopf", "eventTyp"], typen); - setFilter(form.getFieldsValue(true)); - }} - > - {label} - - ); - } + return isBoolean(color) ? ( + + {label} + + ) : ( + + {label} + + ); } return map(labelsColors, (tag) => ); } @@ -108,7 +104,8 @@ export default function TeamFilter() { pushIfSet(teamFilter.technik?.checked, "Technik ist geklärt", ["technik", "checked"]); pushIfSet(teamFilter.technik?.fluegel, "Flügel stimmen", ["technik", "fluegel"]); const eventTypTags = map(teamFilter.kopf?.eventTyp, (typ: string) => eventTypTag(typ)!); - return tags.concat(eventTypTags); + const bookerTags = map(teamFilter.booker, (booker: string) => ({ label: booker, color: "blue", prop: "booker" })); + return tags.concat(eventTypTags).concat(bookerTags); }, [eventTypTag, teamFilter]); const result = [ diff --git a/application/vue/src/components/team/TeamFilter/TeamFilterEdit.tsx b/application/vue/src/components/team/TeamFilter/TeamFilterEdit.tsx index e41a3dfe..fe2bf027 100644 --- a/application/vue/src/components/team/TeamFilter/TeamFilterEdit.tsx +++ b/application/vue/src/components/team/TeamFilter/TeamFilterEdit.tsx @@ -1,12 +1,15 @@ import { Col, Collapse, CollapseProps, ConfigProvider, Form, FormInstance, Row, Space } from "antd"; import ButtonWithIcon from "@/widgets/buttonsAndIcons/ButtonWithIcon.tsx"; import ThreewayCheckbox from "@/widgets/ThreewayCheckbox.tsx"; -import React from "react"; +import React, { useMemo } from "react"; import { TeamFilterObject } from "@/components/team/TeamFilter/applyTeamFilter.ts"; import { useJazzContext } from "@/components/content/useJazzContext.ts"; import { EventTypeMultiSelect } from "@/widgets/EventTypeSelects/EventTypeMultiSelect.tsx"; import { reset } from "@/components/team/TeamFilter/resetTeamFilter.ts"; import { JazzModal } from "@/widgets/JazzModal.tsx"; +import MitarbeiterMultiSelect from "@/widgets/MitarbeiterMultiSelect.tsx"; +import filter from "lodash/filter"; +import map from "lodash/map"; export function TeamFilterEdit({ form, @@ -17,7 +20,11 @@ export function TeamFilterEdit({ readonly open: boolean; readonly setOpen: (open: boolean) => void; }) { - const { setFilter } = useJazzContext(); + const { allUsers, setFilter } = useJazzContext(); + + // eslint-disable-next-line lodash/prop-shorthand + const bookersOnly = useMemo(() => filter(allUsers, (u) => u.accessrights.isBookingTeam), [allUsers]); + const bookersAsOptions = useMemo(() => map(bookersOnly, "asUserAsOption"), [bookersOnly]); const items: CollapseProps["items"] = [ { @@ -31,6 +38,11 @@ export function TeamFilterEdit({ + + + + + diff --git a/application/vue/src/components/team/TeamFilter/applyTeamFilter.ts b/application/vue/src/components/team/TeamFilter/applyTeamFilter.ts index a1698ff1..b5a03595 100644 --- a/application/vue/src/components/team/TeamFilter/applyTeamFilter.ts +++ b/application/vue/src/components/team/TeamFilter/applyTeamFilter.ts @@ -3,6 +3,8 @@ import isEmpty from "lodash/isEmpty"; import isNil from "lodash/isNil"; import { withoutNullOrUndefinedStrippedBy } from "jc-shared/commons/comparingAndTransforming.ts"; import Konzert from "jc-shared/konzert/konzert.ts"; +import intersection from "lodash/intersection"; +import compact from "lodash/compact"; export type TeamFilterObject = { istKonzert?: boolean; @@ -20,6 +22,7 @@ export type TeamFilterObject = { kannInSocialMedia?: boolean; eventTyp?: string[]; }; + booker?: string[]; technik?: { checked?: boolean; fluegel?: boolean }; }; @@ -58,6 +61,14 @@ function filterKopf(ver: Veranstaltung, filterObj: TeamFilterObject) { return pred1 && pred2 && pred3 && pred4 && pred5 && pred6; } +function filterBooker(ver: Veranstaltung, filterObj: TeamFilterObject) { + const filter = filterObj?.booker; + if (isEmpty(filter)) { + return true; + } + return !isEmpty(compact(intersection(filter, ver.booker))); +} + function filterTechnik(ver: Veranstaltung, filterObj: TeamFilterObject) { const filter = filterObj.technik; if (isEmpty(filter)) { @@ -84,6 +95,13 @@ export default function applyTeamFilter(filterOri: TeamFilterObject) { return (ver: Veranstaltung) => { if (!ver || !filter) return true; const artFilter = isNil(filter.istKonzert) ? true : ver.isVermietung === !filter.istKonzert; - return artFilter && filterPresse(ver, filter) && filterKopf(ver, filter) && filterTechnik(ver, filter) && filterUnterkunft(ver, filter); + return ( + artFilter && + filterPresse(ver, filter) && + filterKopf(ver, filter) && + filterBooker(ver, filter) && + filterTechnik(ver, filter) && + filterUnterkunft(ver, filter) + ); }; } diff --git a/application/vue/src/components/team/TeamFilter/resetTeamFilter.ts b/application/vue/src/components/team/TeamFilter/resetTeamFilter.ts index 9a894ae5..0a50d676 100644 --- a/application/vue/src/components/team/TeamFilter/resetTeamFilter.ts +++ b/application/vue/src/components/team/TeamFilter/resetTeamFilter.ts @@ -15,5 +15,6 @@ export function reset(form: FormInstance) { eventTyp: undefined, }, technik: { checked: undefined, fluegel: undefined }, + booker: [], }); } diff --git a/application/vue/test/components/team/applyTeamFilter.test.ts b/application/vue/test/components/team/applyTeamFilter.test.ts index faaf208d..58cc4cdb 100644 --- a/application/vue/test/components/team/applyTeamFilter.test.ts +++ b/application/vue/test/components/team/applyTeamFilter.test.ts @@ -30,13 +30,11 @@ const hotelNichtBestatigt = new Konzert({ unterkunft: { bestaetigt: false }, }); -const mitEventTyp1 = new Konzert({ - kopf: { titel: "mitEventTyp1", eventTyp: "eventTyp1" }, -}); +const mitEventTyp1 = new Konzert({ kopf: { titel: "mitEventTyp1", eventTyp: "eventTyp1" } }); +const mitEventTyp2 = new Konzert({ kopf: { titel: "mitEventTyp2", eventTyp: "eventTyp2" } }); -const mitEventTyp2 = new Konzert({ - kopf: { titel: "mitEventTyp2", eventTyp: "eventTyp2" }, -}); +const booker1 = new Konzert({ kopf: { titel: "booker1" }, booker: ["user1"] }); +const booker2 = new Konzert({ kopf: { titel: "booker2" }, booker: ["user2"] }); const alleKonzerte = [ neutral, @@ -55,6 +53,8 @@ const alleKonzerte = [ hotelNichtBestatigt, mitEventTyp1, mitEventTyp2, + booker1, + booker2, ]; function checkResult(teamFilter: (ver: Veranstaltung) => boolean) { @@ -188,4 +188,19 @@ describe("applyTeamFilter", () => { const filter = applyTeamFilter({ kopf: { eventTyp: ["eventTyp1", "eventTyp2"] } }); expect(checkResult(filter)).to.eql(["mitEventTyp1", "mitEventTyp2"]); }); + + it("should return for one eventTyp", () => { + const filter = applyTeamFilter({ kopf: { eventTyp: ["eventTyp1"] } }); + expect(checkResult(filter)).to.eql(["mitEventTyp1"]); + }); + + it("should return for booker1", () => { + const filter = applyTeamFilter({ booker: ["user1"] }); + expect(checkResult(filter)).to.eql(["booker1"]); + }); + + it("should return for many bookers", () => { + const filter = applyTeamFilter({ booker: ["user1", "user2"] }); + expect(checkResult(filter)).to.eql(["booker1", "booker2"]); + }); }); From fef7b8f221f4c5584ac196d3dd9a94cd3e9dd7a5 Mon Sep 17 00:00:00 2001 From: leider Date: Sun, 23 Mar 2025 12:42:09 +0100 Subject: [PATCH 09/13] fix gruppen type in accessrights --- application/shared/user/accessrights.ts | 22 +++++++++---------- .../konzert/allgemeines/EventCard.tsx | 3 +-- .../team/TeamFilter/TeamFilterEdit.tsx | 3 +-- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/application/shared/user/accessrights.ts b/application/shared/user/accessrights.ts index 4ba995bc..48563590 100644 --- a/application/shared/user/accessrights.ts +++ b/application/shared/user/accessrights.ts @@ -1,42 +1,42 @@ import User, { ABENDKASSE, BOOKING, ORGA, SUPERUSERS } from "./user.js"; export default class Accessrights { - private user?: User; + private user: User; constructor(user?: User) { - this.user = user; + this.user = user ?? new User({}); } - get member(): User | undefined { + get member(): User { return this.user; } get memberId(): string { - return this.member?.id || ""; + return this.member.id || ""; } - get gruppen(): string[] { - return [this.member?.gruppen ?? ""]; + get gruppen(): typeof SUPERUSERS | typeof ORGA | typeof BOOKING | typeof ABENDKASSE | "" { + return this.member.gruppen ?? ""; } get rechte(): string[] { - return this.member?.rechte || []; + return this.member.rechte; } get isSuperuser(): boolean { - return this.gruppen.includes(SUPERUSERS); + return this.gruppen === SUPERUSERS; } get isBookingTeam(): boolean { - return this.isSuperuser || this.gruppen.includes(BOOKING); + return this.isSuperuser || this.gruppen === BOOKING; } get isOrgaTeam(): boolean { - return this.isBookingTeam || this.gruppen.includes(ORGA); + return this.isBookingTeam || this.gruppen === ORGA; } get isAbendkasse(): boolean { - return this.isOrgaTeam || this.gruppen.includes(ABENDKASSE); + return this.isOrgaTeam || this.gruppen === ABENDKASSE; } get darfKasseFreigeben(): boolean { diff --git a/application/vue/src/components/konzert/allgemeines/EventCard.tsx b/application/vue/src/components/konzert/allgemeines/EventCard.tsx index 44dcf1e7..365ae018 100644 --- a/application/vue/src/components/konzert/allgemeines/EventCard.tsx +++ b/application/vue/src/components/konzert/allgemeines/EventCard.tsx @@ -39,8 +39,7 @@ export default function EventCard() { } }, [currentUser, form, id, url]); - // eslint-disable-next-line lodash/prop-shorthand - const bookersOnly = useMemo(() => filter(allUsers, (u) => u.accessrights.isBookingTeam), [allUsers]); + const bookersOnly = useMemo(() => filter(allUsers, "accessrights.isBookingTeam"), [allUsers]); const bookersAsOptions = useMemo(() => map(bookersOnly, "asUserAsOption"), [bookersOnly]); const isBookingTeam = useMemo(() => currentUser.accessrights.isBookingTeam, [currentUser.accessrights.isBookingTeam]); diff --git a/application/vue/src/components/team/TeamFilter/TeamFilterEdit.tsx b/application/vue/src/components/team/TeamFilter/TeamFilterEdit.tsx index fe2bf027..0f83e80a 100644 --- a/application/vue/src/components/team/TeamFilter/TeamFilterEdit.tsx +++ b/application/vue/src/components/team/TeamFilter/TeamFilterEdit.tsx @@ -22,8 +22,7 @@ export function TeamFilterEdit({ }) { const { allUsers, setFilter } = useJazzContext(); - // eslint-disable-next-line lodash/prop-shorthand - const bookersOnly = useMemo(() => filter(allUsers, (u) => u.accessrights.isBookingTeam), [allUsers]); + const bookersOnly = useMemo(() => filter(allUsers, "accessrights.isBookingTeam"), [allUsers]); const bookersAsOptions = useMemo(() => map(bookersOnly, "asUserAsOption"), [bookersOnly]); const items: CollapseProps["items"] = [ From 35a795271fd5b16d713c2f393a589e4ed0550ebd Mon Sep 17 00:00:00 2001 From: leider Date: Sun, 23 Mar 2025 14:06:14 +0100 Subject: [PATCH 10/13] add booker to teamfilter --- .../components/team/TeamFilter/TeamFilter.tsx | 81 ++++++++++--------- .../team/TeamFilter/TeamFilterEdit.tsx | 22 ++--- 2 files changed, 49 insertions(+), 54 deletions(-) diff --git a/application/vue/src/components/team/TeamFilter/TeamFilter.tsx b/application/vue/src/components/team/TeamFilter/TeamFilter.tsx index e72ab669..9a58220f 100644 --- a/application/vue/src/components/team/TeamFilter/TeamFilter.tsx +++ b/application/vue/src/components/team/TeamFilter/TeamFilter.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useMemo, useState } from "react"; -import { Form, Tag } from "antd"; +import { Form, FormInstance, Tag } from "antd"; import ButtonWithIcon from "@/widgets/buttonsAndIcons/ButtonWithIcon.tsx"; import { TeamFilterObject } from "./applyTeamFilter.ts"; import { useJazzContext } from "@/components/content/useJazzContext.ts"; @@ -20,6 +20,33 @@ type LabelColorProperty = { readonly prop?: NamePath; }; +function HeaderTag({ label, color, prop, form }: LabelColorProperty & { readonly form: FormInstance }) { + const { setFilter } = useJazzContext(); + + const closePropTag = useCallback(() => { + const values = filter(form.getFieldValue(prop), (value: string) => value !== label); + form.setFieldValue(prop, values); + setFilter(form.getFieldsValue(true)); + }, [form, label, prop, setFilter]); + + const closeBooleanTagForProp = useCallback(() => { + if (prop) { + form.setFieldValue(prop, undefined); + setFilter(form.getFieldsValue(true)); + } + }, [form, prop, setFilter]); + + return isBoolean(color) ? ( + + {label} + + ) : ( + + {label} + + ); +} + export default function TeamFilter() { const [open, setOpen] = useState(false); @@ -31,49 +58,16 @@ export default function TeamFilter() { form.setFieldsValue(filterObj); }, [filterObj, form]); - const closeBooleanTagForProp = useCallback( - (prop?: NamePath) => () => { - if (prop) { - form.setFieldValue(prop, undefined); - setFilter(form.getFieldsValue(true)); - } - }, - [form, setFilter], - ); - - const closePropTag = useCallback( - (label: string, prop?: NamePath) => () => { - const values = filter(form.getFieldValue(prop), (value: string) => value !== label); - form.setFieldValue(prop, values); - setFilter(form.getFieldsValue(true)); - }, - [form, setFilter], - ); - const eventTypTag = useCallback( (typ: string) => { - const result = find(optionen.typenPlus, ["name", typ]); - if (result) { - return { label: result.name, color: result.color, prop: ["kopf", "eventTyp"] }; - } - return undefined; + const result = find(optionen.typenPlus, ["name", typ]) ?? { name: "", color: "" }; + return { label: result.name, color: result.color, prop: ["kopf", "eventTyp"] }; }, [optionen.typenPlus], ); function headerTagsForFilters(labelsColors: LabelColorProperty[]) { - function HeaderTag({ label, color, prop }: LabelColorProperty) { - return isBoolean(color) ? ( - - {label} - - ) : ( - - {label} - - ); - } - return map(labelsColors, (tag) => ); + return map(labelsColors, (tag) => ); } const teamFilter = withoutNullOrUndefinedStrippedBy(filterObj); @@ -103,7 +97,7 @@ export default function TeamFilter() { pushIfSet(teamFilter.kopf?.fotografBestellen, "Fotograf einladen", ["kopf", "fotografBestellen"]); pushIfSet(teamFilter.technik?.checked, "Technik ist geklärt", ["technik", "checked"]); pushIfSet(teamFilter.technik?.fluegel, "Flügel stimmen", ["technik", "fluegel"]); - const eventTypTags = map(teamFilter.kopf?.eventTyp, (typ: string) => eventTypTag(typ)!); + const eventTypTags = map(teamFilter.kopf?.eventTyp, eventTypTag); const bookerTags = map(teamFilter.booker, (booker: string) => ({ label: booker, color: "blue", prop: "booker" })); return tags.concat(eventTypTags).concat(bookerTags); }, [eventTypTag, teamFilter]); @@ -111,7 +105,16 @@ export default function TeamFilter() { const result = [ setOpen(true)} size="small" text="Filter..." type="default" /> - +
setFilter(form.getFieldsValue(true))} + size="small" + style={{ display: "inline" }} + > + +
, ]; if (!isEmpty(taggies)) { diff --git a/application/vue/src/components/team/TeamFilter/TeamFilterEdit.tsx b/application/vue/src/components/team/TeamFilter/TeamFilterEdit.tsx index 0f83e80a..af6929ab 100644 --- a/application/vue/src/components/team/TeamFilter/TeamFilterEdit.tsx +++ b/application/vue/src/components/team/TeamFilter/TeamFilterEdit.tsx @@ -1,4 +1,4 @@ -import { Col, Collapse, CollapseProps, ConfigProvider, Form, FormInstance, Row, Space } from "antd"; +import { Col, Collapse, CollapseProps, ConfigProvider, Row, Space } from "antd"; import ButtonWithIcon from "@/widgets/buttonsAndIcons/ButtonWithIcon.tsx"; import ThreewayCheckbox from "@/widgets/ThreewayCheckbox.tsx"; import React, { useMemo } from "react"; @@ -10,17 +10,11 @@ import { JazzModal } from "@/widgets/JazzModal.tsx"; import MitarbeiterMultiSelect from "@/widgets/MitarbeiterMultiSelect.tsx"; import filter from "lodash/filter"; import map from "lodash/map"; +import useFormInstance from "antd/es/form/hooks/useFormInstance"; -export function TeamFilterEdit({ - form, - open, - setOpen, -}: { - readonly form: FormInstance; - readonly open: boolean; - readonly setOpen: (open: boolean) => void; -}) { +export function TeamFilterEdit({ open, setOpen }: { readonly open: boolean; readonly setOpen: (open: boolean) => void }) { const { allUsers, setFilter } = useJazzContext(); + const form = useFormInstance(); const bookersOnly = useMemo(() => filter(allUsers, "accessrights.isBookingTeam"), [allUsers]); const bookersAsOptions = useMemo(() => map(bookersOnly, "asUserAsOption"), [bookersOnly]); @@ -144,11 +138,9 @@ export function TeamFilterEdit({ } open={open} > -
setFilter(form.getFieldsValue(true))} size="small"> - - - -
+ + + ); } From 2f02aea98ecd6c94d73ed6ebb255b50f6e89bacf Mon Sep 17 00:00:00 2001 From: leider Date: Sun, 23 Mar 2025 14:59:33 +0100 Subject: [PATCH 11/13] move teamfilter to localStorage --- .../components/content/menu/Preferences.tsx | 24 +++++------ .../src/components/content/useJazzContext.ts | 27 ++++++++---- .../team/ExcelMultiExportButton.tsx | 2 +- application/vue/src/components/team/Info.tsx | 4 +- .../components/team/TeamFilter/TeamFilter.tsx | 43 +++++++++++-------- .../team/TeamFilter/TeamFilterEdit.tsx | 6 +-- .../team/useTeamVeranstaltungenCommons.ts | 2 +- 7 files changed, 63 insertions(+), 45 deletions(-) diff --git a/application/vue/src/components/content/menu/Preferences.tsx b/application/vue/src/components/content/menu/Preferences.tsx index 1be5b813..68e2d41a 100644 --- a/application/vue/src/components/content/menu/Preferences.tsx +++ b/application/vue/src/components/content/menu/Preferences.tsx @@ -5,6 +5,18 @@ import { CheckboxGroupProps } from "antd/es/checkbox"; import useJazzPrefs, { JazzPrefs } from "@/app/useJazzPrefs.ts"; import { JazzModal } from "@/widgets/JazzModal.tsx"; +const optionsHellDunkel: CheckboxGroupProps["options"] = [ + { label: "Hell", value: "bright" }, + { label: "Dunkel", value: "dark" }, + { label: "Automatisch", value: "auto" }, +]; + +const optionsKompaktNormal: CheckboxGroupProps["options"] = [ + { label: "Normal", value: "normal" }, + { label: "Kompakt", value: "compact" }, + { label: "Automatisch", value: "auto" }, +]; + export default function Preferences({ isOpen, setIsOpen }: { readonly isOpen: boolean; readonly setIsOpen: (x: boolean) => void }) { const [form] = Form.useForm(); const { setPreferences, getPreferences } = useJazzPrefs(); @@ -14,18 +26,6 @@ export default function Preferences({ isOpen, setIsOpen }: { readonly isOpen: bo form.setFieldsValue(prefs); }, [form, getPreferences]); - const optionsHellDunkel: CheckboxGroupProps["options"] = [ - { label: "Hell", value: "bright" }, - { label: "Dunkel", value: "dark" }, - { label: "Automatisch", value: "auto" }, - ]; - - const optionsKompaktNormal: CheckboxGroupProps["options"] = [ - { label: "Normal", value: "normal" }, - { label: "Kompakt", value: "compact" }, - { label: "Automatisch", value: "auto" }, - ]; - function saveForm() { setPreferences(form.getFieldsValue(true)); } diff --git a/application/vue/src/components/content/useJazzContext.ts b/application/vue/src/components/content/useJazzContext.ts index 3aa94409..742b194d 100644 --- a/application/vue/src/components/content/useJazzContext.ts +++ b/application/vue/src/components/content/useJazzContext.ts @@ -1,4 +1,4 @@ -import { createContext, useContext, useMemo, useState } from "react"; +import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react"; import User from "jc-shared/user/user.ts"; import { useQueries } from "@tanstack/react-query"; import { allUsers, currentUser, konzerteForToday, optionen as optionenLoader, orte as orteLoader, wikisubdirs } from "@/rest/loader.ts"; @@ -21,8 +21,8 @@ const emptyContext: SharedGlobals = { todayKonzerte: [], showSuccess: noop, showError: noop, - filter: {}, - setFilter: noop, + teamFilter: {}, + setTeamFilter: noop, isDirty: false, setIsDirty: noop, setMemoizedId: noop, @@ -39,8 +39,8 @@ type SharedGlobals = { todayKonzerte: Konzert[]; showSuccess: ({ text, title, duration }: { duration?: number; text?: React.ReactNode; title?: string }) => void; showError: ({ text, title, closeCallback }: { text?: string; title?: string; closeCallback?: () => void }) => void; - filter: TeamFilterObject; - setFilter: (filter: TeamFilterObject) => void; + teamFilter: TeamFilterObject; + setTeamFilter: (filter: TeamFilterObject) => void; isDirty: boolean; setIsDirty: (a: boolean) => void; memoizedId?: string; @@ -59,6 +59,15 @@ export function useCreateJazzContext(auth: IUseProvideAuth): SharedGlobals { const refetchInterval = 30 * 60 * 1000; // 30 minutes const [filter, setFilter] = useState({}); + useEffect(() => { + setFilter(JSON.parse(localStorage.getItem("teamFilter") ?? "{}")); + }, []); + + const setTeamFilter = useCallback((filter: TeamFilterObject) => { + localStorage.setItem("teamFilter", JSON.stringify(filter)); + setFilter(filter); + }, []); + const [isDirty, setIsDirty] = useState(false); const [memoizedId, setMemoizedId] = useState(); @@ -66,8 +75,8 @@ export function useCreateJazzContext(auth: IUseProvideAuth): SharedGlobals { SharedGlobals, | "showSuccess" | "showError" - | "filter" - | "setFilter" + | "teamFilter" + | "setTeamFilter" | "isDirty" | "setIsDirty" | "memoizedId" @@ -143,8 +152,8 @@ export function useCreateJazzContext(auth: IUseProvideAuth): SharedGlobals { ...exposedContext, showSuccess, showError, - filter, - setFilter, + teamFilter: filter, + setTeamFilter, isDirty, setIsDirty, memoizedId, diff --git a/application/vue/src/components/team/ExcelMultiExportButton.tsx b/application/vue/src/components/team/ExcelMultiExportButton.tsx index 4a9aa038..86ab3480 100644 --- a/application/vue/src/components/team/ExcelMultiExportButton.tsx +++ b/application/vue/src/components/team/ExcelMultiExportButton.tsx @@ -26,7 +26,7 @@ function SelectRangeForExcelModal({ readonly alle: { startDate: Date }[]; }) { const [form] = useForm(); - const { optionen, filter: teamFilter } = useJazzContext(); + const { optionen, teamFilter } = useJazzContext(); const [first, setFirst] = useState(dayjs()); const [last, setLast] = useState(dayjs()); diff --git a/application/vue/src/components/team/Info.tsx b/application/vue/src/components/team/Info.tsx index 8ad467d2..473f42ff 100644 --- a/application/vue/src/components/team/Info.tsx +++ b/application/vue/src/components/team/Info.tsx @@ -91,7 +91,7 @@ function Uebersicht({ veranstaltungen }: { readonly veranstaltungen: Veranstaltu } export default function Info() { - const { filter: contextFilter } = useJazzContext(); + const { teamFilter } = useJazzContext(); const { monatJahr } = useParams(); // als yymm const [search, setSearch] = useSearchParams(); const [activePage, setActivePage] = useState("pressetexte"); @@ -109,7 +109,7 @@ export default function Info() { queryFn: () => konzerteBetweenYYYYMM(start.yyyyMM, end.yyyyMM), }); - const veranstaltungen = useMemo(() => filter(data, applyTeamFilter(contextFilter)), [data, contextFilter]); + const veranstaltungen = useMemo(() => filter(data, applyTeamFilter(teamFilter)), [data, teamFilter]); useEffect( () => { diff --git a/application/vue/src/components/team/TeamFilter/TeamFilter.tsx b/application/vue/src/components/team/TeamFilter/TeamFilter.tsx index 9a58220f..3f3a7e71 100644 --- a/application/vue/src/components/team/TeamFilter/TeamFilter.tsx +++ b/application/vue/src/components/team/TeamFilter/TeamFilter.tsx @@ -4,7 +4,6 @@ import ButtonWithIcon from "@/widgets/buttonsAndIcons/ButtonWithIcon.tsx"; import { TeamFilterObject } from "./applyTeamFilter.ts"; import { useJazzContext } from "@/components/content/useJazzContext.ts"; import isNil from "lodash/isNil"; -import { withoutNullOrUndefinedStrippedBy } from "jc-shared/commons/comparingAndTransforming.ts"; import isEmpty from "lodash/isEmpty"; import { NamePath } from "rc-field-form/es/interface"; import { TeamFilterEdit } from "@/components/team/TeamFilter/TeamFilterEdit.tsx"; @@ -13,28 +12,30 @@ import isBoolean from "lodash/isBoolean"; import find from "lodash/find"; import map from "lodash/map"; import filter from "lodash/filter"; +import User from "jc-shared/user/user.ts"; type LabelColorProperty = { readonly label: string; readonly color: boolean | string; readonly prop?: NamePath; + readonly value?: string; }; -function HeaderTag({ label, color, prop, form }: LabelColorProperty & { readonly form: FormInstance }) { - const { setFilter } = useJazzContext(); +function HeaderTag({ label, value, color, prop, form }: LabelColorProperty & { readonly form: FormInstance }) { + const { setTeamFilter } = useJazzContext(); const closePropTag = useCallback(() => { - const values = filter(form.getFieldValue(prop), (value: string) => value !== label); + const values = filter(form.getFieldValue(prop), (val: string) => val !== (value ?? label)); form.setFieldValue(prop, values); - setFilter(form.getFieldsValue(true)); - }, [form, label, prop, setFilter]); + setTeamFilter(form.getFieldsValue(true)); + }, [form, label, prop, setTeamFilter, value]); const closeBooleanTagForProp = useCallback(() => { if (prop) { form.setFieldValue(prop, undefined); - setFilter(form.getFieldsValue(true)); + setTeamFilter(form.getFieldsValue(true)); } - }, [form, prop, setFilter]); + }, [form, prop, setTeamFilter]); return isBoolean(color) ? ( @@ -52,11 +53,11 @@ export default function TeamFilter() { const [form] = Form.useForm(); - const { filter: filterObj, setFilter, optionen } = useJazzContext(); + const { teamFilter, setTeamFilter, optionen, allUsers } = useJazzContext(); useEffect(() => { - form.setFieldsValue(filterObj); - }, [filterObj, form]); + form.setFieldsValue(teamFilter); + }, [form, teamFilter]); const eventTypTag = useCallback( (typ: string) => { @@ -67,10 +68,18 @@ export default function TeamFilter() { ); function headerTagsForFilters(labelsColors: LabelColorProperty[]) { - return map(labelsColors, (tag) => ); + return map(labelsColors, (tag) => ( + + )); } - const teamFilter = withoutNullOrUndefinedStrippedBy(filterObj); + const createBookerTag = useCallback( + (booker: string) => { + const bookerUser = find(allUsers, { id: booker }) ?? new User({ name: booker }); + return { label: bookerUser.name, value: booker, color: "blue", prop: "booker" }; + }, + [allUsers], + ); const taggies = useMemo(() => { function pushIfSet(att: boolean | undefined, label: string, prop?: NamePath) { @@ -98,9 +107,9 @@ export default function TeamFilter() { pushIfSet(teamFilter.technik?.checked, "Technik ist geklärt", ["technik", "checked"]); pushIfSet(teamFilter.technik?.fluegel, "Flügel stimmen", ["technik", "fluegel"]); const eventTypTags = map(teamFilter.kopf?.eventTyp, eventTypTag); - const bookerTags = map(teamFilter.booker, (booker: string) => ({ label: booker, color: "blue", prop: "booker" })); + const bookerTags = map(teamFilter.booker, createBookerTag); return tags.concat(eventTypTags).concat(bookerTags); - }, [eventTypTag, teamFilter]); + }, [createBookerTag, eventTypTag, teamFilter]); const result = [ @@ -109,7 +118,7 @@ export default function TeamFilter() { autoComplete="off" colon={false} form={form} - onValuesChange={() => setFilter(form.getFieldsValue(true))} + onValuesChange={() => setTeamFilter(form.getFieldsValue(true))} size="small" style={{ display: "inline" }} > @@ -124,7 +133,7 @@ export default function TeamFilter() { key="resetFilter" onClick={() => { reset(form); - setFilter(form.getFieldsValue(true)); + setTeamFilter(form.getFieldsValue(true)); }} size="small" text="Zurücksetzen" diff --git a/application/vue/src/components/team/TeamFilter/TeamFilterEdit.tsx b/application/vue/src/components/team/TeamFilter/TeamFilterEdit.tsx index af6929ab..8d9286cd 100644 --- a/application/vue/src/components/team/TeamFilter/TeamFilterEdit.tsx +++ b/application/vue/src/components/team/TeamFilter/TeamFilterEdit.tsx @@ -13,7 +13,7 @@ import map from "lodash/map"; import useFormInstance from "antd/es/form/hooks/useFormInstance"; export function TeamFilterEdit({ open, setOpen }: { readonly open: boolean; readonly setOpen: (open: boolean) => void }) { - const { allUsers, setFilter } = useJazzContext(); + const { allUsers, setTeamFilter } = useJazzContext(); const form = useFormInstance(); const bookersOnly = useMemo(() => filter(allUsers, "accessrights.isBookingTeam"), [allUsers]); @@ -121,7 +121,7 @@ export function TeamFilterEdit({ open, setOpen }: { readonly open: boolean; read alwaysText onClick={() => { reset(form); - setFilter(form.getFieldsValue(true)); + setTeamFilter(form.getFieldsValue(true)); }} text="Zurücksetzen" type="default" @@ -130,7 +130,7 @@ export function TeamFilterEdit({ open, setOpen }: { readonly open: boolean; read alwaysText onClick={() => { setOpen(false); - setFilter(form.getFieldsValue(true)); + setTeamFilter(form.getFieldsValue(true)); }} text="Schließen" /> diff --git a/application/vue/src/components/team/useTeamVeranstaltungenCommons.ts b/application/vue/src/components/team/useTeamVeranstaltungenCommons.ts index ad56fd1e..1d467984 100644 --- a/application/vue/src/components/team/useTeamVeranstaltungenCommons.ts +++ b/application/vue/src/components/team/useTeamVeranstaltungenCommons.ts @@ -17,7 +17,7 @@ import keys from "lodash/keys"; export const useTeamVeranstaltungenCommons = (periodsToShow: string[]) => { const [search, setSearch] = useSearchParams(); - const { allUsers, filter: teamFilter } = useJazzContext(); + const { allUsers, teamFilter } = useJazzContext(); const [period, setPeriod] = useState("Zukünftige"); From 5761ace258d9cdbc137e53d73bfe7cf8fd13d979 Mon Sep 17 00:00:00 2001 From: leider Date: Sun, 23 Mar 2025 15:14:20 +0100 Subject: [PATCH 12/13] fix UI test --- frontendtests/tests/20_filter_test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontendtests/tests/20_filter_test.ts b/frontendtests/tests/20_filter_test.ts index c6c18cd5..3bd803c6 100644 --- a/frontendtests/tests/20_filter_test.ts +++ b/frontendtests/tests/20_filter_test.ts @@ -100,4 +100,5 @@ Data(menuToClick).Scenario("Viele'", async ({ I, current, filters }) => { I.see("HotelNichtBestatigt"); I.dontSee("HotelBestaetigt"); I.dontSee("Neutral"); + I.click(locate("button").withText("Zurücksetzen").inside(".ant-space-item")); }); From 600e7774fb76769768762976f13791b106c7bdaf Mon Sep 17 00:00:00 2001 From: leider Date: Sun, 23 Mar 2025 17:28:17 +0100 Subject: [PATCH 13/13] move period to localStorage --- application/vue/src/components/team/Team.tsx | 2 +- .../team/TeamUndVeranstaltungen.tsx | 8 +-- .../src/components/team/Veranstaltungen.tsx | 2 +- .../team/useTeamVeranstaltungenCommons.ts | 53 ++++++++++++------- application/vue/src/rest/loader.ts | 5 +- 5 files changed, 45 insertions(+), 25 deletions(-) diff --git a/application/vue/src/components/team/Team.tsx b/application/vue/src/components/team/Team.tsx index a97fdcf5..197ba78e 100644 --- a/application/vue/src/components/team/Team.tsx +++ b/application/vue/src/components/team/Team.tsx @@ -6,5 +6,5 @@ export default function Team() { useDirtyBlocker(false); document.title = "Team"; - return ; + return ; } diff --git a/application/vue/src/components/team/TeamUndVeranstaltungen.tsx b/application/vue/src/components/team/TeamUndVeranstaltungen.tsx index 1b622b09..89f147e7 100644 --- a/application/vue/src/components/team/TeamUndVeranstaltungen.tsx +++ b/application/vue/src/components/team/TeamUndVeranstaltungen.tsx @@ -10,9 +10,11 @@ import { TeamContext } from "@/components/team/TeamContext.ts"; import { useTeamVeranstaltungenCommons } from "@/components/team/useTeamVeranstaltungenCommons.ts"; import { useJazzContext } from "@/components/content/useJazzContext.ts"; import map from "lodash/map"; +import { useLocation } from "react-router"; -export function TeamUndVeranstaltungen({ periodsToShow }: { readonly periodsToShow: string[] }) { +export function TeamUndVeranstaltungen() { const { memoizedId } = useJazzContext(); + const { pathname } = useLocation(); useEffect(() => { setTimeout(() => { const element = document.getElementById(memoizedId ?? ""); @@ -25,9 +27,9 @@ export function TeamUndVeranstaltungen({ periodsToShow }: { readonly periodsToSh }, 1000); }, [memoizedId]); - const forVeranstaltungen = useMemo(() => periodsToShow.includes("alle"), [periodsToShow]); + const forVeranstaltungen = useMemo(() => pathname === "/veranstaltungen", [pathname]); const { period, periods, veranstaltungen, veranstaltungenNachMonat, monate, filterTags, usersAsOptions } = - useTeamVeranstaltungenCommons(periodsToShow); + useTeamVeranstaltungenCommons(); const subState = useMemo(() => ({ veranstaltungenNachMonat, usersAsOptions }), [usersAsOptions, veranstaltungenNachMonat]); diff --git a/application/vue/src/components/team/Veranstaltungen.tsx b/application/vue/src/components/team/Veranstaltungen.tsx index 05660e65..2a332dcf 100644 --- a/application/vue/src/components/team/Veranstaltungen.tsx +++ b/application/vue/src/components/team/Veranstaltungen.tsx @@ -6,5 +6,5 @@ export default function Veranstaltungen() { useDirtyBlocker(false); document.title = "Veranstaltungen"; - return ; + return ; } diff --git a/application/vue/src/components/team/useTeamVeranstaltungenCommons.ts b/application/vue/src/components/team/useTeamVeranstaltungenCommons.ts index 1d467984..4fa48f39 100644 --- a/application/vue/src/components/team/useTeamVeranstaltungenCommons.ts +++ b/application/vue/src/components/team/useTeamVeranstaltungenCommons.ts @@ -1,6 +1,5 @@ -import { useSearchParams } from "react-router"; import { useJazzContext } from "@/components/content/useJazzContext.ts"; -import { useEffect, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { useQueries } from "@tanstack/react-query"; import { konzerteForTeam, vermietungenForTeam } from "@/rest/loader.ts"; import Veranstaltung from "jc-shared/veranstaltung/veranstaltung.ts"; @@ -9,37 +8,55 @@ import reverse from "lodash/reverse"; import applyTeamFilter from "@/components/team/TeamFilter/applyTeamFilter.ts"; import groupBy from "lodash/groupBy"; import TeamFilter from "@/components/team/TeamFilter/TeamFilter.tsx"; -import find from "lodash/find"; import map from "lodash/map"; import filter from "lodash/filter"; import capitalize from "lodash/capitalize"; import keys from "lodash/keys"; -export const useTeamVeranstaltungenCommons = (periodsToShow: string[]) => { - const [search, setSearch] = useSearchParams(); - const { allUsers, teamFilter } = useJazzContext(); +export type Period = "zukuenftige" | "vergangene" | "alle"; + +export const useTeamVeranstaltungenCommons = () => { + const [search, setSearch] = useState("zukuenftige"); + const { allUsers, currentUser, teamFilter } = useJazzContext(); + + const periodsToShow = useMemo( + () => (currentUser.accessrights.isOrgaTeam ? ["zukuenftige", "vergangene", "alle"] : ["zukuenftige", "vergangene"]) as Period[], + [currentUser], + ); + + const setSelectedPeriod = useCallback((period: Period) => { + localStorage.setItem("veranstaltungenPeriod", period); + setSearch(period); + }, []); + + useEffect(() => { + const period = (localStorage.getItem("veranstaltungenPeriod") ?? "zukuenftige") as Period; + if (periodsToShow.includes(period)) { + setSearch(period); + } else { + setSearch("zukuenftige"); + } + }, [periodsToShow]); const [period, setPeriod] = useState("Zukünftige"); const periods = useMemo(() => { return map(periodsToShow, (period) => { - return { label: period === "zukuenftige" ? "Zukünftige" : capitalize(period), key: period, onClick: () => setSearch({ period }) }; + return { + label: period === "zukuenftige" ? "Zukünftige" : capitalize(period), + key: period, + onClick: () => setSelectedPeriod(period), + }; }); - }, [periodsToShow, setSearch]); + }, [periodsToShow, setSelectedPeriod]); - const selectedPeriod: "zukuenftige" | "vergangene" | "alle" = useMemo(() => { - return (search.get("period") || periods[0].key) as "zukuenftige" | "vergangene" | "alle"; + const selectedPeriod: Period = useMemo(() => { + return search || periods[0].key; }, [periods, search]); useEffect(() => { - const result = find(periods, ["key", search.get("period")]); - if (!result) { - setSearch({ period: periods[0].key }); - setPeriod("Zukünftige"); - } else { - setPeriod(result.label); - } - }, [periods, search, setSearch]); + setPeriod(selectedPeriod === "zukuenftige" ? "Zukünftige" : capitalize(selectedPeriod)); + }, [selectedPeriod]); const queryResult = useQueries({ queries: [ diff --git a/application/vue/src/rest/loader.ts b/application/vue/src/rest/loader.ts index ab807c45..b935b29b 100644 --- a/application/vue/src/rest/loader.ts +++ b/application/vue/src/rest/loader.ts @@ -22,6 +22,7 @@ import KonzertWithRiderBoxes from "jc-shared/konzert/konzertWithRiderBoxes.ts"; import { historyFromRawRows } from "@/rest/historyObject.ts"; import { refreshTokenPost } from "@/rest/authenticationRequests.ts"; import sortBy from "lodash/sortBy"; +import { Period } from "@/components/team/useTeamVeranstaltungenCommons.ts"; type ContentType = "pdf" | "zip" | "other"; @@ -86,7 +87,7 @@ export async function konzerteForToday() { return handleVeranstaltungen(result); } -export async function konzerteForTeam(selector: "zukuenftige" | "vergangene" | "alle") { +export async function konzerteForTeam(selector: Period) { const result = await get({ url: `/konzerte/${selector}`, resType: [new Konzert()] }); return handleVeranstaltungen(result); } @@ -139,7 +140,7 @@ function handleVermietungen(result?: Vermietung[]): Vermietung[] { return map(result, (each) => new Vermietung(each)); } -export async function vermietungenForTeam(selector: "zukuenftige" | "vergangene" | "alle") { +export async function vermietungenForTeam(selector: Period) { const result = await get({ url: `/vermietungen/${selector}`, resType: [new Vermietung()] }); return handleVermietungen(result); }