diff --git a/application/vue/src/components/mails/SendMail.tsx b/application/vue/src/components/mails/SendMail.tsx index 950a5e6b..857ad8f9 100644 --- a/application/vue/src/components/mails/SendMail.tsx +++ b/application/vue/src/components/mails/SendMail.tsx @@ -46,7 +46,7 @@ export default function SendMail() { const { mailRules, veranstaltungen } = useQueries({ queries: [ - { queryKey: ["konzert", "zukuenftige"], queryFn: () => konzerteForTeam("zukuenftige") }, + { queryKey: ["konzert", "zukuenftige"], queryFn: () => konzerteForTeam("Zukünftige") }, { queryKey: ["mailRules"], queryFn: mailRulesRestCall }, ], combine: ([a, b]) => { diff --git a/application/vue/src/components/options/imageoverview/useCreateImagenamesSections.ts b/application/vue/src/components/options/imageoverview/useCreateImagenamesSections.ts index 6562dc87..1d299d98 100644 --- a/application/vue/src/components/options/imageoverview/useCreateImagenamesSections.ts +++ b/application/vue/src/components/options/imageoverview/useCreateImagenamesSections.ts @@ -24,7 +24,7 @@ export function useCreateImagenamesSections() { const { imagenames, veranstaltungen } = useQueries({ queries: [ { queryKey: ["imagenames"], queryFn: imagenamesQuery }, - { queryKey: ["konzert", "alle"], queryFn: () => konzerteForTeam("alle") }, + { queryKey: ["konzert", "alle"], queryFn: () => konzerteForTeam("Alle") }, ], combine: ([a, b]) => { if (a?.data && b?.data) { diff --git a/application/vue/src/components/team/TeamPeriodsSelector.tsx b/application/vue/src/components/team/TeamPeriodsSelector.tsx new file mode 100644 index 00000000..5a8dd176 --- /dev/null +++ b/application/vue/src/components/team/TeamPeriodsSelector.tsx @@ -0,0 +1,44 @@ +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { Button, Dropdown, Space } from "antd"; +import { IconForSmallBlock } from "@/widgets/buttonsAndIcons/Icon.tsx"; +import { useJazzContext } from "@/components/content/useJazzContext.ts"; +import map from "lodash/map"; +import { Period } from "@/components/team/useTeamVeranstaltungenCommons.ts"; + +export function TeamPeriodsSelector() { + const { currentUser } = useJazzContext(); + const [period, setPeriod] = useState("Zukünftige"); + + const periodsToShow = useMemo( + () => + (currentUser.accessrights.isOrgaTeam + ? ["Zukünftige", "Vergangene", "Alle", "Alle (voll)"] + : ["Zukünftige", "Vergangene"]) as Period[], + [currentUser], + ); + + const items = useMemo(() => map(periodsToShow, (period) => ({ label: period, key: period })), [periodsToShow]); + + useEffect(() => { + const per = (localStorage.getItem("veranstaltungenPeriod") ?? "Zukünftige") as Period; + const adaptedPeriod = periodsToShow.includes(per) ? per : "Zukünftige"; + setPeriod(adaptedPeriod); + }, [periodsToShow]); + + const onClick = useCallback(({ key }: { key: string }) => { + setPeriod(key as Period); + localStorage.setItem("veranstaltungenPeriod", key as Period); + window.dispatchEvent(new Event("storage")); + }, []); + + return ( + + + + ); +} diff --git a/application/vue/src/components/team/TeamTable.tsx b/application/vue/src/components/team/TeamTable.tsx new file mode 100644 index 00000000..47130c82 --- /dev/null +++ b/application/vue/src/components/team/TeamTable.tsx @@ -0,0 +1,98 @@ +import * as React from "react"; +import { useCallback, useContext, useEffect, useMemo, useRef } from "react"; +import { ConfigProvider, Table, TableProps } from "antd"; +import Veranstaltung from "jc-shared/veranstaltung/veranstaltung.ts"; +import { TeamContext } from "@/components/team/TeamContext.ts"; +import map from "lodash/map"; +import keys from "lodash/keys"; +import forEach from "lodash/forEach"; +import { Link } from "react-router"; +import { TableRef } from "antd/es/table"; +import { useJazzContext } from "@/components/content/useJazzContext.ts"; +import "./teamTable.css"; +const columns: TableProps["columns"] = [ + { + title: "Typ", + dataIndex: "typ", + render: (value: TypInRow) => {value.name}, + sorter: (a, b) => a.typ.name.localeCompare(b.typ.name), + }, + { + title: "Datum", + dataIndex: "datum", + render: (value: string, record) => ( + {value} + ), + sorter: (a, b) => a.isoDate.localeCompare(b.isoDate), + }, + { + title: "Name", + dataIndex: "name", + filterSearch: true, + render: (value: string, record) => {value}, + }, +]; + +type TypInRow = { name: string; color: string }; + +type VeranstaltungAsRow = { + key: string; + datum: string; + isoDate: string; + name: string; + typ: TypInRow; + url: string; +}; + +function veranstaltungToRow(veranstaltung: Veranstaltung): VeranstaltungAsRow { + return { + key: veranstaltung.id ?? "", + datum: veranstaltung.startDatumUhrzeit.mitUhrzeitNumerisch, + isoDate: veranstaltung.startDate.toISOString(), + name: veranstaltung.kopf.titelMitPrefix, + typ: { name: veranstaltung.kopf.eventTypRich?.name ?? "Vermietung", color: veranstaltung.kopf.eventTypRich?.color ?? "black" }, + url: veranstaltung?.fullyQualifiedUrl, + }; +} + +function nachMonatToRows(veranstaltungenNachMonat: { [p: string]: Veranstaltung[] }) { + const monate = keys(veranstaltungenNachMonat); + let result: VeranstaltungAsRow[] = []; + forEach(monate, (monat) => { + const current = veranstaltungenNachMonat[monat]; + result.push({ + key: monat, + datum: monat, + isoDate: current[0].startDatumUhrzeit.setTag(0).toISOString, + name: "", + typ: { name: "", color: "black" }, + url: "", + }); + const rows = map(current, veranstaltungToRow); + result = result.concat(rows); + }); + return result; +} + +export default function TeamTable() { + const { veranstaltungenNachMonat } = useContext(TeamContext); + const { memoizedVeranstaltung } = useJazzContext(); + + const data = useMemo(() => nachMonatToRows(veranstaltungenNachMonat), [veranstaltungenNachMonat]); + + const tableRef = useRef(null); + + const memoId = useMemo(() => memoizedVeranstaltung?.veranstaltung?.id, [memoizedVeranstaltung?.veranstaltung?.id]); + + useEffect(() => { + tableRef.current?.scrollTo({ key: memoId }); + }, [memoId]); + + const rowClassName = useCallback((record: VeranstaltungAsRow) => (record.key === memoId ? "table-row-highlight" : ""), [memoId]); + + return ( + + + + ); +} diff --git a/application/vue/src/components/team/TeamUndVeranstaltungen.tsx b/application/vue/src/components/team/TeamUndVeranstaltungen.tsx index f3b1e358..bfd235d3 100644 --- a/application/vue/src/components/team/TeamUndVeranstaltungen.tsx +++ b/application/vue/src/components/team/TeamUndVeranstaltungen.tsx @@ -1,9 +1,8 @@ import React, { useEffect, useMemo } from "react"; -import { Button, Col, Dropdown, Row, Space } from "antd"; +import { Col, Row } from "antd"; import { JazzPageHeader } from "@/widgets/JazzPageHeader.tsx"; import ExcelMultiExportButton from "@/components/team/ExcelMultiExportButton.tsx"; import { NewButtons } from "@/components/colored/JazzButtons.tsx"; -import { IconForSmallBlock } from "@/widgets/buttonsAndIcons/Icon.tsx"; import TeamCalendar from "@/components/team/TeamCalendar.tsx"; import TeamMonatGroup from "@/components/team/TeamMonatGroup.tsx"; import { TeamContext } from "@/components/team/TeamContext.ts"; @@ -15,6 +14,8 @@ import { useJazzContext } from "@/components/content/useJazzContext.ts"; import useCalcHeight from "@/components/team/useCalcHeight.ts"; import ScrollingContent from "@/components/content/ScrollingContent.tsx"; import filter from "lodash/filter"; +import { TeamPeriodsSelector } from "@/components/team/TeamPeriodsSelector.tsx"; +import TeamTable from "@/components/team/TeamTable.tsx"; function Monate({ monate }: { monate: string[] }) { return map(monate, (monat) => ); @@ -25,14 +26,13 @@ export function TeamUndVeranstaltungen() { const { memoizedVeranstaltung, setMemoizedVeranstaltung } = useJazzContext(); const calcHeight = useCalcHeight(); const forVeranstaltungen = useMemo(() => pathname === "/veranstaltungen", [pathname]); - const { period, periods, veranstaltungen, veranstaltungenNachMonat, monate, usersAsOptions, filtered } = useTeamVeranstaltungenCommons(); + const { veranstaltungen, veranstaltungenNachMonat, monate, usersAsOptions, filtered, period } = useTeamVeranstaltungenCommons(); const teamContext = useMemo(() => { const alleErsthelfer = map( filter(usersAsOptions, (u) => u.kann.includes("Ersthelfer")), "value", ); - return { veranstaltungenNachMonat, usersAsOptions, period, calcHeight, noOfVeranstaltungen: filtered.length, alleErsthelfer }; }, [veranstaltungenNachMonat, usersAsOptions, period, calcHeight, filtered.length]); @@ -49,28 +49,14 @@ export function TeamUndVeranstaltungen() { buttons={[ forVeranstaltungen && , forVeranstaltungen && , - - - , + , , ]} tags={} title={forVeranstaltungen ? "Veranstaltungen" : "Team"} /> - - - + {period === "Alle" ? : } diff --git a/application/vue/src/components/team/teamTable.css b/application/vue/src/components/team/teamTable.css new file mode 100644 index 00000000..7d905a40 --- /dev/null +++ b/application/vue/src/components/team/teamTable.css @@ -0,0 +1,3 @@ +.table-row-highlight { + background-color: var(--ant-color-success-bg); +} diff --git a/application/vue/src/components/team/useTeamVeranstaltungenCommons.ts b/application/vue/src/components/team/useTeamVeranstaltungenCommons.ts index 4e3c5342..e4e01c5f 100644 --- a/application/vue/src/components/team/useTeamVeranstaltungenCommons.ts +++ b/application/vue/src/components/team/useTeamVeranstaltungenCommons.ts @@ -1,5 +1,5 @@ import { useJazzContext } from "@/components/content/useJazzContext.ts"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { 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,58 +9,34 @@ import applyTeamFilter from "@/components/team/TeamFilter/applyTeamFilter.ts"; import groupBy from "lodash/groupBy"; import map from "lodash/map"; import filter from "lodash/filter"; -import capitalize from "lodash/capitalize"; import keys from "lodash/keys"; import forEach from "lodash/forEach"; -export type Period = "zukuenftige" | "vergangene" | "alle"; +export type Period = "Zukünftige" | "Vergangene" | "Alle" | "Alle (voll)"; 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 { allUsers, teamFilter } = useJazzContext(); - const setSelectedPeriod = useCallback((period: Period) => { - localStorage.setItem("veranstaltungenPeriod", period); - setSearch(period); - }, []); + const [period, setPeriod] = useState("Zukünftige"); 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: () => setSelectedPeriod(period), - }; - }); - }, [periodsToShow, setSelectedPeriod]); - - const selectedPeriod: Period = useMemo(() => { - return search || periods[0].key; - }, [periods, search]); - - useEffect(() => { - setPeriod(selectedPeriod === "zukuenftige" ? "Zukünftige" : capitalize(selectedPeriod)); - }, [selectedPeriod]); + setPeriod((localStorage.getItem("veranstaltungenPeriod") ?? "Zukünftige") as Period); + const listener = () => { + const newPeriod = (localStorage.getItem("veranstaltungenPeriod") ?? "Zukünftige") as Period; + if (period !== newPeriod) { + setPeriod(newPeriod); + } + }; + window.addEventListener("storage", listener); + return () => { + window.removeEventListener("storage", listener); + }; + }, [period]); const queryResult = useQueries({ queries: [ - { queryKey: ["konzert", selectedPeriod], queryFn: () => konzerteForTeam(selectedPeriod) }, - { queryKey: ["vermietung", selectedPeriod], queryFn: () => vermietungenForTeam(selectedPeriod) }, + { queryKey: ["konzert", period], queryFn: () => konzerteForTeam(period) }, + { queryKey: ["vermietung", period], queryFn: () => vermietungenForTeam(period) }, ], combine: ([a, b]) => { if (a?.data && b?.data) { @@ -73,8 +49,8 @@ export const useTeamVeranstaltungenCommons = () => { const veranstaltungen = useMemo(() => { const additionals = queryResult.flatMap((res) => res.createGhostsForOverview() as Veranstaltung[]); const sortedAscending = sortBy(queryResult.concat(additionals), "startDate") as Veranstaltung[]; - return selectedPeriod !== "zukuenftige" ? reverse(sortedAscending) : sortedAscending; - }, [queryResult, selectedPeriod]); + return period !== "Zukünftige" ? reverse(sortedAscending) : sortedAscending; + }, [queryResult, period]); const usersAsOptions = useMemo(() => map(allUsers, "asUserAsOption"), [allUsers]); @@ -91,9 +67,7 @@ export const useTeamVeranstaltungenCommons = () => { return groups; }, [filtered]); - const monate = useMemo(() => { - return keys(veranstaltungenNachMonat); - }, [veranstaltungenNachMonat]); + const monate = useMemo(() => keys(veranstaltungenNachMonat), [veranstaltungenNachMonat]); - return { period, usersAsOptions, veranstaltungenNachMonat, monate, periods, veranstaltungen, filtered }; + return { period, usersAsOptions, veranstaltungenNachMonat, monate, veranstaltungen, filtered }; }; diff --git a/application/vue/src/rest/loader.ts b/application/vue/src/rest/loader.ts index 5706bc1f..6fe5cc8c 100644 --- a/application/vue/src/rest/loader.ts +++ b/application/vue/src/rest/loader.ts @@ -87,8 +87,14 @@ export async function konzerteForToday() { return handleVeranstaltungen(result); } +const transformer = { + Zukünftige: "zukuenftige", + Vergangene: "vergangene", + Alle: "alle", + "Alle (voll)": "alle", +}; export async function konzerteForTeam(selector: Period) { - const result = await get({ url: `/konzerte/${selector}`, resType: [new Konzert()] }); + const result = await get({ url: `/konzerte/${transformer[selector]}`, resType: [new Konzert()] }); return handleVeranstaltungen(result); } @@ -141,7 +147,7 @@ function handleVermietungen(result?: Vermietung[]): Vermietung[] { } export async function vermietungenForTeam(selector: Period) { - const result = await get({ url: `/vermietungen/${selector}`, resType: [new Vermietung()] }); + const result = await get({ url: `/vermietungen/${transformer[selector]}`, resType: [new Vermietung()] }); return handleVermietungen(result); }