From c1c6cd96da0ce12a7281f0cb84bd29b111b41bcc Mon Sep 17 00:00:00 2001 From: Tobias Date: Mon, 28 Oct 2024 14:53:03 +0100 Subject: [PATCH 01/32] Introduce contact table A new UI to mass create and update contacts. --- package-lock.json | 45 ++++ package.json | 1 + .../table/_components/ContactsTable.tsx | 207 ++++++++++++++++++ .../[projectSlug]/contacts/table/page.tsx | 24 ++ src/app/(loggedInProjects)/layout.tsx | 13 +- src/pagesComponents/contacts/ContactTable.tsx | 11 +- src/server/contacts/queries/getContacts.ts | 3 + 7 files changed, 298 insertions(+), 6 deletions(-) create mode 100644 src/app/(loggedInProjects)/[projectSlug]/contacts/table/_components/ContactsTable.tsx create mode 100644 src/app/(loggedInProjects)/[projectSlug]/contacts/table/page.tsx diff --git a/package-lock.json b/package-lock.json index 47ae9db8..e20af628 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,6 +43,7 @@ "postcss": "8.4.49", "prisma": "5.22.0", "react": "18.3.1", + "react-datasheet-grid": "4.11.4", "react-dom": "18.3.1", "react-hook-form": "7.53.2", "react-intl": "6.8.9", @@ -11439,6 +11440,12 @@ "node": ">= 0.4" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", + "license": "MIT" + }, "node_modules/cli-boxes": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", @@ -20807,6 +20814,22 @@ "node": ">=0.10.0" } }, + "node_modules/react-datasheet-grid": { + "version": "4.11.4", + "resolved": "https://registry.npmjs.org/react-datasheet-grid/-/react-datasheet-grid-4.11.4.tgz", + "integrity": "sha512-fWSOOHCPAv1Qkdk3+Io/Z8b02NC+V1Q/4FIAbhEAv9GmwaBAu/B68pySstY7eIDbgVLxnDLiAq8oCt98HZ8FHg==", + "license": "MIT", + "dependencies": { + "@tanstack/react-virtual": "^3.0.0-beta.18", + "classnames": "^2.3.1", + "fast-deep-equal": "^3.1.3", + "react-resize-detector": "^7.1.2", + "throttle-debounce": "^3.0.1" + }, + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-devtools-core": { "version": "4.28.5", "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-4.28.5.tgz", @@ -21507,6 +21530,19 @@ "react": ">=16.8" } }, + "node_modules/react-resize-detector": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/react-resize-detector/-/react-resize-detector-7.1.2.tgz", + "integrity": "sha512-zXnPJ2m8+6oq9Nn8zsep/orts9vQv3elrpA+R8XTcW7DVVUJ9vwDwMXaBtykAYjMnkCIaOoK9vObyR7ZgFNlOw==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-smooth": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.1.tgz", @@ -23787,6 +23823,15 @@ "node": ">=0.8" } }, + "node_modules/throttle-debounce": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz", + "integrity": "sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/through2": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/through2/-/through2-3.0.2.tgz", diff --git a/package.json b/package.json index c9289911..c3e45a75 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "postcss": "8.4.49", "prisma": "5.22.0", "react": "18.3.1", + "react-datasheet-grid": "4.11.4", "react-dom": "18.3.1", "react-hook-form": "7.53.2", "react-intl": "6.8.9", diff --git a/src/app/(loggedInProjects)/[projectSlug]/contacts/table/_components/ContactsTable.tsx b/src/app/(loggedInProjects)/[projectSlug]/contacts/table/_components/ContactsTable.tsx new file mode 100644 index 00000000..850f8d47 --- /dev/null +++ b/src/app/(loggedInProjects)/[projectSlug]/contacts/table/_components/ContactsTable.tsx @@ -0,0 +1,207 @@ +"use client" +import { ObjectDump } from "@/src/app/admin/_components/ObjectDump" +import { improveErrorMessage } from "@/src/core/components/forms/improveErrorMessage" +import { pinkButtonStyles } from "@/src/core/components/links" +import { ButtonWrapper } from "@/src/core/components/links/ButtonWrapper" +import { useProjectSlug } from "@/src/core/routes/useProjectSlug" +import { isProduction } from "@/src/core/utils" +import createContact from "@/src/server/contacts/mutations/createContact" +import updateContact from "@/src/server/contacts/mutations/updateContact" +import getContacts, { TContacts } from "@/src/server/contacts/queries/getContacts" +import { getQueryClient, getQueryKey, useMutation, useQuery } from "@blitzjs/rpc" +import { useState } from "react" +import { Column, DataSheetGrid, keyColumn, textColumn } from "react-datasheet-grid" +import "react-datasheet-grid/dist/style.css" + +type Row = { + id: string | null // Required + firstName: string | null + lastName: string | null // Required + email: string | null // Required + phone: string | null + note: string | null + role: string | null +} + +export const ContactsTable = () => { + const projectSlug = useProjectSlug() + + // To disable the automatic form refetching that useQuery does once we change something in the form. + const [formDirty, setFormDirty] = useState(false) + // This is hacky. We need to refetch the data after the mutation, + // which we do by invalidating the query and using onSucess (in usePaginatedQuery) to set the fresh data. + // However this breaks react during the first render, when `setData` is not present, yet. + // The workaround is to manually manage this state and only allow the update when we hit save. + const [performUpdate, setPerformUpdate] = useState(false) + + const prepareData = (data: TContacts | undefined) => { + if (!data) return [] + return data.contacts.map(({ id, firstName, lastName, email, phone, note, role }) => { + return { id: String(id), firstName, lastName, email, phone, note, role } satisfies Row + }) + } + + const queryKey = getQueryKey(getContacts, { projectSlug }) + const [contacts] = useQuery( + getContacts, + { projectSlug }, + { + enabled: !formDirty, + onSuccess: (data) => { + if (performUpdate) { + setData(prepareData(data)) + } + }, + }, + ) + + const [data, setData] = useState(prepareData(contacts)) + const [errors, setErrors] = useState<[string, string][]>([]) + + const [createContactMutation, { isSuccess: isCreateSuccess }] = useMutation(createContact) + const [updateContactMutation, { isSuccess: isUpdateSuccess }] = useMutation(updateContact) + + const NEW_ID_VALUE = "NEU" + const columns: Column[] = [ + { + ...keyColumn("id", textColumn), + title: "ID", + disabled: true, + maxWidth: 60, + }, + { + ...keyColumn("firstName", textColumn), + title: "Vorname", + }, + { + ...keyColumn("lastName", textColumn), + title: "Nachname (Pflicht)", + }, + { + ...keyColumn("email", textColumn), + title: "E-Mail-Adresse (Pflicht)", + }, + { + ...keyColumn("phone", textColumn), + title: "Telefonnummer", + }, + { + ...keyColumn("note", textColumn), + title: "Notizen (Markdown)", + }, + { + ...keyColumn("role", textColumn), + title: "Position", + }, + ] + + const handleUpdate = async () => { + // Reset Errors + setErrors([]) + + // Only refetch, if all changes are stored. + // This allows us to manually show errors in the form and we can resubmit them. + let refetchData = false + + for (const { id, lastName, email, ...value } of data) { + // Manually handle "required" fields errors + if (!lastName || !email) { + setErrors((prev) => [ + ...prev, + [String(id || NEW_ID_VALUE), "Nachname und E-Mail-Adresse sind Pflichtfelder."], + ]) + continue + } + + const createOrUpdateMutation = + !id || id === NEW_ID_VALUE + ? { action: createContactMutation, additionalData: {} } + : { action: updateContactMutation, additionalData: { id: Number(id) } } + + await createOrUpdateMutation.action( + // @ts-expect-error TS is not able to infer that `id` should not be part of `create` + { ...value, lastName, email, projectSlug, ...createOrUpdateMutation.additionalData }, + { + onError: (error, updatedContacts, context) => { + refetchData = false + console.log("ERROR", error, updatedContacts, context) + setErrors((prev) => [ + ...prev, + [ + String(id) || NEW_ID_VALUE, + improveErrorMessage(error, "FORM_ERROR", ["email"]) as any, + ], + ]) + }, + onSuccess: () => { + refetchData = true + }, + }, + ) + } + + if (refetchData) { + if (!isProduction) { + console.log("INFO", "update and create both where successfull, so refetching the data now") + } + setPerformUpdate(true) + setFormDirty(false) + await getQueryClient().invalidateQueries(queryKey) + } + } + + return ( + <> +
+ {errors.length > 0 ? ( +
    + {errors.map(([id, errors]) => { + const strings: string[] = + typeof errors === "object" ? Object.values(errors).filter(Boolean) : [errors] + return ( +
  • + ID {id} +
    + {strings.map((string) => { + return

    {string}

    + })} + +
    +
  • + ) + })} +
+ ) : ( +
+ )} + + + {!formDirty && (isCreateSuccess || isUpdateSuccess) && ( + Gespeichert + )} + + +
+ + ({ + id: NEW_ID_VALUE, + firstName: null, + lastName: null, + email: null, + phone: null, + note: null, + role: null, + })} + onChange={(values) => { + setFormDirty(true) + setData(values) + }} + columns={columns} + /> + + ) +} diff --git a/src/app/(loggedInProjects)/[projectSlug]/contacts/table/page.tsx b/src/app/(loggedInProjects)/[projectSlug]/contacts/table/page.tsx new file mode 100644 index 00000000..faf5a9ef --- /dev/null +++ b/src/app/(loggedInProjects)/[projectSlug]/contacts/table/page.tsx @@ -0,0 +1,24 @@ +import { PageHeader } from "@/src/core/components/pages/PageHeader" +import { Metadata } from "next" +import "server-only" +import { ContactsTable } from "./_components/ContactsTable" + +export const metadata: Metadata = { + title: "Externe Kontakte bearbeiten & importieren", +} + +export default async function ProjectContactsTablePage() { + return ( + <> + + {/* We cannot use here because that is strongly tied to the pages router */} + {/* */} + + + + ) +} diff --git a/src/app/(loggedInProjects)/layout.tsx b/src/app/(loggedInProjects)/layout.tsx index 93aef562..540fd6b9 100644 --- a/src/app/(loggedInProjects)/layout.tsx +++ b/src/app/(loggedInProjects)/layout.tsx @@ -1,5 +1,7 @@ import { useAuthenticatedBlitzContext } from "@/src/blitz-server" import type { Metadata } from "next" +import { FooterMinimal } from "../_components/layouts/footer/FooterMinimal" +import { NavigationLoggedInProject } from "../_components/layouts/navigation/NavigationLoggedInProject" export const metadata: Metadata = { robots: "noindex", @@ -12,5 +14,14 @@ export const metadata: Metadata = { export default async function LoggedInProjectsLayout({ children }: { children: React.ReactNode }) { await useAuthenticatedBlitzContext({ redirectTo: "/auth/login" }) - return <>{children} + return ( + <> +
+ +
{children}
+
+ {/* <-- get nicht, da es auf pages directory helper zurück greift */} + + + ) } diff --git a/src/pagesComponents/contacts/ContactTable.tsx b/src/pagesComponents/contacts/ContactTable.tsx index e1dccf01..aabe3ac7 100644 --- a/src/pagesComponents/contacts/ContactTable.tsx +++ b/src/pagesComponents/contacts/ContactTable.tsx @@ -10,7 +10,7 @@ import getProject from "@/src/server/projects/queries/getProject" import { useCurrentUser } from "@/src/server/users/hooks/useCurrentUser" import { Routes } from "@blitzjs/next" import { useQuery } from "@blitzjs/rpc" -import { PencilSquareIcon, TrashIcon } from "@heroicons/react/20/solid" +import { TrashIcon } from "@heroicons/react/20/solid" import { Contact } from "@prisma/client" import { useRouter } from "next/router" import { useState } from "react" @@ -105,7 +105,8 @@ export const ContactTable = ({ contacts }: Props) => {
- + {/* Disabled in favor of the contacts/table UI */} + {/* { Bearbeiten - + */} { - - Kontakt + + Kontakte hinzufügen & bearbeiten
- +
+ + + Status verwalten… + +
+
Date: Wed, 29 Jan 2025 11:13:49 +0100 Subject: [PATCH 12/32] Project: include ids, date and status in geo export api response --- src/app/api/projects/[slug]/route.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/app/api/projects/[slug]/route.ts b/src/app/api/projects/[slug]/route.ts index f0e21d7d..af97fd8b 100644 --- a/src/app/api/projects/[slug]/route.ts +++ b/src/app/api/projects/[slug]/route.ts @@ -21,11 +21,12 @@ export async function GET(request: Request, { params }: { params: Promise<{ slug const subsections = await db.subsection.findMany({ where: { project: { slug } }, select: { + slug: true, geometry: true, + // do we want to have the slug here as well? operator: { select: { title: true } }, - // these only exist in the subsubsection model - // estimatedCompletionDate: true, - // SubsubsectionStatus: { select: { slug: true } }, + estimatedCompletionDate: true, + SubsubsectionStatus: { select: { slug: true, title: true } }, }, }) @@ -33,6 +34,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ slug return new Response("No subsections found for the given project", { status: 404 }) } + // we validate the linestring format on create and update with zod, so this should not be necessary, but the databse only "knows" that it is a json field const validSubsections = subsections.filter( (s) => s.geometry && Array.isArray(s.geometry) && s.geometry.length > 0, ) @@ -44,9 +46,13 @@ export async function GET(request: Request, { params }: { params: Promise<{ slug const projectFeatures = featureCollection( validSubsections.map((s) => lineString(s.geometry as [number, number][], { + subsectionSlug: s.slug, + projectSlug: slug, operator: s.operator ? s.operator.title : null, - // estimatedCompletionDate: s.estimatedCompletionDate, - // status: s.SubsubsectionStatus ? s.SubsubsectionStatus.slug : null, + estimatedCompletionDate: s.estimatedCompletionDate, + status: s.SubsubsectionStatus + ? { slug: s.SubsubsectionStatus.slug, title: s.SubsubsectionStatus.title } + : null, }), ), ) From 6adc5f1ea058016d1fbf243b76479959b6bd467f Mon Sep 17 00:00:00 2001 From: JohannaPeanut <76495099+JohannaPeanut@users.noreply.github.com> Date: Wed, 29 Jan 2025 11:30:20 +0100 Subject: [PATCH 13/32] Project: standardize error logs in geo export api --- src/app/api/projects/[slug]/route.ts | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/app/api/projects/[slug]/route.ts b/src/app/api/projects/[slug]/route.ts index af97fd8b..1b22192c 100644 --- a/src/app/api/projects/[slug]/route.ts +++ b/src/app/api/projects/[slug]/route.ts @@ -12,10 +12,17 @@ export async function GET(request: Request, { params }: { params: Promise<{ slug }) if (!project) { - return new Response("No project found for that slug", { status: 404 }) + console.error(`No project found for slug: ${slug}`) + return new Response(JSON.stringify({ error: "No project found for that slug" }), { + status: 404, + }) } if (!project.isExportApi) { - return new Response("The export for this project is disabled by the admin", { status: 404 }) + console.error(`Export API is disabled for project with slug: ${slug}`) + return new Response( + JSON.stringify({ error: "The export for this project is disabled by the admin" }), + { status: 404 }, + ) } const subsections = await db.subsection.findMany({ @@ -31,7 +38,10 @@ export async function GET(request: Request, { params }: { params: Promise<{ slug }) if (subsections?.length === 0) { - return new Response("No subsections found for the given project", { status: 404 }) + console.error(`No subsections found for project with slug: ${slug}`) + return new Response(JSON.stringify({ error: "No subsections found for the given project" }), { + status: 404, + }) } // we validate the linestring format on create and update with zod, so this should not be necessary, but the databse only "knows" that it is a json field @@ -40,7 +50,11 @@ export async function GET(request: Request, { params }: { params: Promise<{ slug ) if (validSubsections.length === 0) { - return new Response("No valid subsections found for the given project", { status: 404 }) + console.error(`No valid subsections found for project with slug: ${slug}`) + return new Response( + JSON.stringify({ error: "No valid subsections found for the given project" }), + { status: 404 }, + ) } const projectFeatures = featureCollection( @@ -62,6 +76,6 @@ export async function GET(request: Request, { params }: { params: Promise<{ slug }) } catch (error) { console.error("Error fetching subsections:", error) - return new Response("Internal Server Error", { status: 500 }) + return new Response(JSON.stringify({ error: "Internal Server Error" }), { status: 500 }) } } From 313a78d4b03fbb1233bd98ed95d59c99c10bd93d Mon Sep 17 00:00:00 2001 From: JohannaPeanut <76495099+JohannaPeanut@users.noreply.github.com> Date: Thu, 30 Jan 2025 16:39:31 +0100 Subject: [PATCH 14/32] Project: rename field export enabled --- .../migration.sql | 9 +++++++++ db/schema.prisma | 2 +- db/seeds/projects.ts | 8 ++++---- .../[projectSlug]/edit/_components/ProjectForm.tsx | 2 +- .../_components/AdminEnableProjectExportApi.tsx | 12 ++++++------ src/app/admin/projects/page.tsx | 5 ++++- src/app/api/projects/[slug]/route.ts | 4 ++-- .../projects/mutations/updateProjectExportApi.ts | 6 +++--- src/server/projects/schema.ts | 2 +- tests/blitz/mocks/mockProject.ts | 2 +- 10 files changed, 32 insertions(+), 20 deletions(-) create mode 100644 db/migrations/20250130144729_project_rename_export_enabled/migration.sql diff --git a/db/migrations/20250130144729_project_rename_export_enabled/migration.sql b/db/migrations/20250130144729_project_rename_export_enabled/migration.sql new file mode 100644 index 00000000..1e6289a8 --- /dev/null +++ b/db/migrations/20250130144729_project_rename_export_enabled/migration.sql @@ -0,0 +1,9 @@ +/* + Warnings: + + - You are about to drop the column `isExportApi` on the `Project` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Project" DROP COLUMN "isExportApi", +ADD COLUMN "exportEnabled" BOOLEAN NOT NULL DEFAULT false; diff --git a/db/schema.prisma b/db/schema.prisma index fe62bd88..3970678e 100644 --- a/db/schema.prisma +++ b/db/schema.prisma @@ -134,7 +134,7 @@ model Project { logoSrc String? partnerLogoSrcs String[] felt_subsection_geometry_source_url String? - isExportApi Boolean @default(false) + exportEnabled Boolean @default(false) // manager User? @relation(fields: [managerId], references: [id]) managerId Int? diff --git a/db/seeds/projects.ts b/db/seeds/projects.ts index eddc1521..986e4251 100644 --- a/db/seeds/projects.ts +++ b/db/seeds/projects.ts @@ -12,7 +12,7 @@ Sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, logoSrc: "rsv8-logo.png", felt_subsection_geometry_source_url: null, partnerLogoSrcs: ["rsv8-logo.png", "test.png"], - isExportApi: false, + exportEnabled: false, }, { slug: "rs3000", @@ -22,7 +22,7 @@ Sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, logoSrc: null, felt_subsection_geometry_source_url: null, partnerLogoSrcs: [], - isExportApi: false, + exportEnabled: false, }, { slug: "rs0v1", @@ -32,7 +32,7 @@ Sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, logoSrc: null, felt_subsection_geometry_source_url: null, partnerLogoSrcs: [], - isExportApi: false, + exportEnabled: false, }, { slug: "rs0v2", @@ -42,7 +42,7 @@ Sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, logoSrc: null, felt_subsection_geometry_source_url: null, partnerLogoSrcs: [], - isExportApi: false, + exportEnabled: false, }, ] diff --git a/src/app/(loggedInProjects)/[projectSlug]/edit/_components/ProjectForm.tsx b/src/app/(loggedInProjects)/[projectSlug]/edit/_components/ProjectForm.tsx index bcd072dd..568e5ed7 100644 --- a/src/app/(loggedInProjects)/[projectSlug]/edit/_components/ProjectForm.tsx +++ b/src/app/(loggedInProjects)/[projectSlug]/edit/_components/ProjectForm.tsx @@ -57,7 +57,7 @@ export const ProjectForm = ({ users, ...props }: Props) => { placeholder="https://felt.com/map/beispiel-karte" help="Die Felt-Karte muss dem Account info@fixmycity.de gehören." /> - + { - const [isExportApiEnabled, setIsExportApiEnabled] = useState(isExportApi) +export const AdminEnableProjectExportApi = ({ slug, exportEnabled }: Props) => { + const [exportEnabledEnabled, setExportEnabledEnabled] = useState(exportEnabled) const [createProjectMutation] = useMutation(updateProjectExportApi) const handleCheckboxChange = async (event: React.ChangeEvent) => { const isChecked = event.target.checked - setIsExportApiEnabled(isChecked) + setExportEnabledEnabled(isChecked) try { - await createProjectMutation({ isExportApi: isChecked, projectSlug: slug }) + await createProjectMutation({ exportEnabled: isChecked, projectSlug: slug }) } catch (error: any) { console.error("Error updating project / enable export API:", error) } @@ -29,7 +29,7 @@ export const AdminEnableProjectExportApi = ({ slug, isExportApi }: Props) => { Export-API aktiv diff --git a/src/app/admin/projects/page.tsx b/src/app/admin/projects/page.tsx index 5e5a6ca8..787bba1b 100644 --- a/src/app/admin/projects/page.tsx +++ b/src/app/admin/projects/page.tsx @@ -31,7 +31,10 @@ export default async function AdminProjectsPage() { Planungsabschnitte - +
{JSON.stringify(project, undefined, 2)}
) diff --git a/src/app/api/projects/[slug]/route.ts b/src/app/api/projects/[slug]/route.ts index 1b22192c..94f8a624 100644 --- a/src/app/api/projects/[slug]/route.ts +++ b/src/app/api/projects/[slug]/route.ts @@ -7,7 +7,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ slug const project = await db.project.findFirst({ where: { slug }, select: { - isExportApi: true, + exportEnabled: true, }, }) @@ -17,7 +17,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ slug status: 404, }) } - if (!project.isExportApi) { + if (!project.exportEnabled) { console.error(`Export API is disabled for project with slug: ${slug}`) return new Response( JSON.stringify({ error: "The export for this project is disabled by the admin" }), diff --git a/src/server/projects/mutations/updateProjectExportApi.ts b/src/server/projects/mutations/updateProjectExportApi.ts index daafdcbe..f9fea610 100644 --- a/src/server/projects/mutations/updateProjectExportApi.ts +++ b/src/server/projects/mutations/updateProjectExportApi.ts @@ -3,17 +3,17 @@ import { resolver } from "@blitzjs/rpc" import { z } from "zod" const UpdateProjectExportApi = z.object({ - isExportApi: z.coerce.boolean(), + exportEnabled: z.coerce.boolean(), projectSlug: z.string(), }) export default resolver.pipe( resolver.zod(UpdateProjectExportApi), resolver.authorize("ADMIN"), - async ({ isExportApi, projectSlug }) => { + async ({ exportEnabled, projectSlug }) => { return await db.project.update({ where: { slug: projectSlug }, - data: { isExportApi }, + data: { exportEnabled }, }) }, ) diff --git a/src/server/projects/schema.ts b/src/server/projects/schema.ts index 2d9071ce..8caae5c1 100644 --- a/src/server/projects/schema.ts +++ b/src/server/projects/schema.ts @@ -9,7 +9,7 @@ export const ProjectSchema = z.object({ partnerLogoSrcs: z.array(z.string()).nullish(), felt_subsection_geometry_source_url: z.union([z.string().url().nullish(), z.literal("")]), managerId: InputNumberOrNullSchema, - isExportApi: z.coerce.boolean(), + exportEnabled: z.coerce.boolean(), }) export type ProjectType = z.infer diff --git a/tests/blitz/mocks/mockProject.ts b/tests/blitz/mocks/mockProject.ts index ae3a86bd..deb29c69 100644 --- a/tests/blitz/mocks/mockProject.ts +++ b/tests/blitz/mocks/mockProject.ts @@ -13,5 +13,5 @@ Sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, partnerLogoSrcs: ["rsv8-logo.png", "test.png"], createdAt: new Date(), updatedAt: new Date(), - isExportApi: false, + exportEnabled: false, } From 56789332af16d5f2d30e6a329a04307198f2a386 Mon Sep 17 00:00:00 2001 From: JohannaPeanut <76495099+JohannaPeanut@users.noreply.github.com> Date: Thu, 30 Jan 2025 16:42:02 +0100 Subject: [PATCH 15/32] Form: standardize LabeledCheckboxes --- .../[projectSlug]/edit/_components/ProjectForm.tsx | 3 ++- src/app/admin/surveys/_components/AdminSurveyForm.tsx | 2 +- src/pagesComponents/subsubsections/SubsubsectionForm.tsx | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/app/(loggedInProjects)/[projectSlug]/edit/_components/ProjectForm.tsx b/src/app/(loggedInProjects)/[projectSlug]/edit/_components/ProjectForm.tsx index 568e5ed7..870fd654 100644 --- a/src/app/(loggedInProjects)/[projectSlug]/edit/_components/ProjectForm.tsx +++ b/src/app/(loggedInProjects)/[projectSlug]/edit/_components/ProjectForm.tsx @@ -57,7 +57,8 @@ export const ProjectForm = ({ users, ...props }: Props) => { placeholder="https://felt.com/map/beispiel-karte" help="Die Felt-Karte muss dem Account info@fixmycity.de gehören." /> - + {/* @ts-expect-error the defaults work fine; but the helper should be updated at some point */} + { + {/* @ts-expect-error the defaults work fine; but the helper should be updated at some point */} >(props: FormProp - {/* @ts-expect-error the defaults work fine; but the helper should be update at some point */} + {/* @ts-expect-error the defaults work fine; but the helper should be updated at some point */}
Date: Thu, 30 Jan 2025 16:47:37 +0100 Subject: [PATCH 16/32] Project: remove error cases, replace console.error with .log in geo export --- src/app/api/projects/[slug]/route.ts | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/src/app/api/projects/[slug]/route.ts b/src/app/api/projects/[slug]/route.ts index 94f8a624..5d904c19 100644 --- a/src/app/api/projects/[slug]/route.ts +++ b/src/app/api/projects/[slug]/route.ts @@ -12,13 +12,13 @@ export async function GET(request: Request, { params }: { params: Promise<{ slug }) if (!project) { - console.error(`No project found for slug: ${slug}`) + console.log("No project found for slug: ", slug) return new Response(JSON.stringify({ error: "No project found for that slug" }), { status: 404, }) } if (!project.exportEnabled) { - console.error(`Export API is disabled for project with slug: ${slug}`) + console.log("Export is disabled for project with slug: ", slug) return new Response( JSON.stringify({ error: "The export for this project is disabled by the admin" }), { status: 404 }, @@ -37,28 +37,8 @@ export async function GET(request: Request, { params }: { params: Promise<{ slug }, }) - if (subsections?.length === 0) { - console.error(`No subsections found for project with slug: ${slug}`) - return new Response(JSON.stringify({ error: "No subsections found for the given project" }), { - status: 404, - }) - } - - // we validate the linestring format on create and update with zod, so this should not be necessary, but the databse only "knows" that it is a json field - const validSubsections = subsections.filter( - (s) => s.geometry && Array.isArray(s.geometry) && s.geometry.length > 0, - ) - - if (validSubsections.length === 0) { - console.error(`No valid subsections found for project with slug: ${slug}`) - return new Response( - JSON.stringify({ error: "No valid subsections found for the given project" }), - { status: 404 }, - ) - } - const projectFeatures = featureCollection( - validSubsections.map((s) => + subsections.map((s) => lineString(s.geometry as [number, number][], { subsectionSlug: s.slug, projectSlug: slug, From f481e060bdaf42da6c258406f6ff1e87cb748aa0 Mon Sep 17 00:00:00 2001 From: JohannaPeanut <76495099+JohannaPeanut@users.noreply.github.com> Date: Thu, 30 Jan 2025 16:49:39 +0100 Subject: [PATCH 17/32] Project: update response object in geo export --- src/app/api/projects/[slug]/route.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/app/api/projects/[slug]/route.ts b/src/app/api/projects/[slug]/route.ts index 5d904c19..4c8f627a 100644 --- a/src/app/api/projects/[slug]/route.ts +++ b/src/app/api/projects/[slug]/route.ts @@ -42,11 +42,9 @@ export async function GET(request: Request, { params }: { params: Promise<{ slug lineString(s.geometry as [number, number][], { subsectionSlug: s.slug, projectSlug: slug, - operator: s.operator ? s.operator.title : null, + operator: s.operator?.title, estimatedCompletionDate: s.estimatedCompletionDate, - status: s.SubsubsectionStatus - ? { slug: s.SubsubsectionStatus.slug, title: s.SubsubsectionStatus.title } - : null, + status: s.SubsubsectionStatus?.title, }), ), ) From 80d4567b4350774c0717c792055e810e3ee4e72f Mon Sep 17 00:00:00 2001 From: JohannaPeanut <76495099+JohannaPeanut@users.noreply.github.com> Date: Thu, 30 Jan 2025 17:07:33 +0100 Subject: [PATCH 18/32] Project: replace enableExport checkbox --- .../AdminEnableProjectExportApi.tsx | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/src/app/admin/projects/[projectSlug]/subsections/_components/AdminEnableProjectExportApi.tsx b/src/app/admin/projects/[projectSlug]/subsections/_components/AdminEnableProjectExportApi.tsx index 6e7a35b1..3d6f46d2 100644 --- a/src/app/admin/projects/[projectSlug]/subsections/_components/AdminEnableProjectExportApi.tsx +++ b/src/app/admin/projects/[projectSlug]/subsections/_components/AdminEnableProjectExportApi.tsx @@ -1,7 +1,8 @@ "use client" +import { blueButtonStyles } from "@/src/core/components/links" import updateProjectExportApi from "@/src/server/projects/mutations/updateProjectExportApi" import { useMutation } from "@blitzjs/rpc" -import React, { useState } from "react" +import { useState } from "react" type Props = { slug: string @@ -9,31 +10,25 @@ type Props = { } export const AdminEnableProjectExportApi = ({ slug, exportEnabled }: Props) => { - const [exportEnabledEnabled, setExportEnabledEnabled] = useState(exportEnabled) + const [isExportEnabled, setIsExportEnabled] = useState(exportEnabled) const [createProjectMutation] = useMutation(updateProjectExportApi) - const handleCheckboxChange = async (event: React.ChangeEvent) => { - const isChecked = event.target.checked - setExportEnabledEnabled(isChecked) - + const handleEnableExportClick = async () => { + const newExportEnabledState = !isExportEnabled + setIsExportEnabled(newExportEnabledState) try { - await createProjectMutation({ exportEnabled: isChecked, projectSlug: slug }) + await createProjectMutation({ exportEnabled: newExportEnabledState, projectSlug: slug }) } catch (error: any) { - console.error("Error updating project / enable export API:", error) + console.error("Error updating project / enable export: ", error) + setIsExportEnabled(!newExportEnabledState) } } return (
- +
) } From cd1d4ca012dd614a509210c35c6ded4eede6d279 Mon Sep 17 00:00:00 2001 From: JohannaPeanut <76495099+JohannaPeanut@users.noreply.github.com> Date: Tue, 4 Feb 2025 11:22:57 +0100 Subject: [PATCH 19/32] Subsection: update admin geo import with placemark --- .env.local.example | 3 - .github/README.md | 2 - .github/workflows/setup-env.yml | 2 - .../migration.sql | 9 ++ db/schema.prisma | 26 +++--- db/seeds/projects.ts | 8 +- .../edit/_components/ProjectForm.tsx | 8 +- ...Felt.tsx => SubsectionPlacemarkImport.tsx} | 43 ++++++---- .../_components/SubsectionTableAdmin.tsx | 6 +- .../[projectSlug]/subsections/page.tsx | 4 +- .../forms/LabeledTextFieldCalculateLength.tsx | 2 +- src/env.d.ts | 2 - .../abschnitte/[subsectionSlug]/edit.tsx | 2 +- src/pages/[projectSlug]/abschnitte/new.tsx | 2 +- src/pages/[projectSlug]/index.tsx | 2 +- .../subsections/SubsectionForm.tsx | 28 ++---- src/server/projects/schema.ts | 2 +- .../updateSubsectionsWithFeltData.ts | 86 ------------------- .../updateSubsectionsWithPlacemark.ts | 73 ++++++++++++++++ src/server/subsections/schema.ts | 9 +- tests/blitz/mocks/mockProject.ts | 2 +- 21 files changed, 149 insertions(+), 172 deletions(-) create mode 100644 db/migrations/20250203085504_proejct_rename_column_placemark_url/migration.sql rename src/app/admin/projects/[projectSlug]/subsections/_components/{SubsectionFelt.tsx => SubsectionPlacemarkImport.tsx} (61%) delete mode 100644 src/server/subsections/mutations/updateSubsectionsWithFeltData.ts create mode 100644 src/server/subsections/mutations/updateSubsectionsWithPlacemark.ts diff --git a/.env.local.example b/.env.local.example index 25055fbe..c46e16f2 100644 --- a/.env.local.example +++ b/.env.local.example @@ -23,9 +23,6 @@ S3_UPLOAD_ROOTFOLDER=upload-localdev S3_UPLOAD_KEY=AAAAAAAAAAAAAAAAAAAA S3_UPLOAD_SECRET=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -# Get from https://felt.com/ => Settings -FELT_TOKEN=felt_pat_AAAAAAAAAAAAAAAAAAAAAA - # Admin mail address, mails are sent to ADMIN_EMAIL=hello@trassenscout.de diff --git a/.github/README.md b/.github/README.md index 3c18d8fe..810ec4b3 100644 --- a/.github/README.md +++ b/.github/README.md @@ -39,8 +39,6 @@ | | | | | `ADMIN_EMAIL` | `vars.ADMIN_EMAIL` | | | | | | -| `FELT_TOKEN` | `secrets.FELT_TOKEN` | | -| | | | | `S3_UPLOAD_KEY` | `secrets.S3_UPLOAD_KEY` | | | `S3_UPLOAD_SECRET` | `secrets.S3_UPLOAD_SECRET` | | | `S3_UPLOAD_ROOTFOLDER` | `.env`-variable | In `.env` as composite value | diff --git a/.github/workflows/setup-env.yml b/.github/workflows/setup-env.yml index c8c253b2..1716bdf8 100644 --- a/.github/workflows/setup-env.yml +++ b/.github/workflows/setup-env.yml @@ -56,8 +56,6 @@ jobs: ADMIN_EMAIL='${{ vars.ADMIN_EMAIL }}' - FELT_TOKEN='${{ secrets.FELT_TOKEN }}' - S3_UPLOAD_KEY='${{ secrets.S3_UPLOAD_KEY }}' S3_UPLOAD_SECRET='${{ secrets.S3_UPLOAD_SECRET }}' S3_UPLOAD_ROOTFOLDER='upload-${{ inputs.ENVIRONMENT }}' diff --git a/db/migrations/20250203085504_proejct_rename_column_placemark_url/migration.sql b/db/migrations/20250203085504_proejct_rename_column_placemark_url/migration.sql new file mode 100644 index 00000000..8b1e2fcd --- /dev/null +++ b/db/migrations/20250203085504_proejct_rename_column_placemark_url/migration.sql @@ -0,0 +1,9 @@ +/* + Warnings: + + - You are about to drop the column `felt_subsection_geometry_source_url` on the `Project` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Project" DROP COLUMN "felt_subsection_geometry_source_url", +ADD COLUMN "placemarkUrl" TEXT; diff --git a/db/schema.prisma b/db/schema.prisma index 2908cce0..46d02c16 100644 --- a/db/schema.prisma +++ b/db/schema.prisma @@ -124,19 +124,19 @@ model Token { // } model Project { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - // - slug String @unique // shortTitle and URL Slug - subTitle String? - description String? - logoSrc String? - partnerLogoSrcs String[] - felt_subsection_geometry_source_url String? - // - manager User? @relation(fields: [managerId], references: [id]) - managerId Int? + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + // + slug String @unique // shortTitle and URL Slug + subTitle String? + description String? + logoSrc String? + partnerLogoSrcs String[] + placemarkUrl String? + // + manager User? @relation(fields: [managerId], references: [id]) + managerId Int? surveyResponseTopics SurveyResponseTopic[] subsections Subsection[] diff --git a/db/seeds/projects.ts b/db/seeds/projects.ts index 3b044b53..46015f95 100644 --- a/db/seeds/projects.ts +++ b/db/seeds/projects.ts @@ -10,7 +10,7 @@ const seedProjects = async () => { Sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr.`, managerId: null, logoSrc: "rsv8-logo.png", - felt_subsection_geometry_source_url: null, + placemarkUrl: null, partnerLogoSrcs: ["rsv8-logo.png", "test.png"], }, { @@ -19,7 +19,7 @@ Sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, description: null, managerId: null, logoSrc: null, - felt_subsection_geometry_source_url: null, + placemarkUrl: null, partnerLogoSrcs: [], }, { @@ -28,7 +28,7 @@ Sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, description: null, managerId: null, logoSrc: null, - felt_subsection_geometry_source_url: null, + placemarkUrl: null, partnerLogoSrcs: [], }, { @@ -37,7 +37,7 @@ Sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, description: null, managerId: null, logoSrc: null, - felt_subsection_geometry_source_url: null, + placemarkUrl: null, partnerLogoSrcs: [], }, ] diff --git a/src/app/(loggedInProjects)/[projectSlug]/edit/_components/ProjectForm.tsx b/src/app/(loggedInProjects)/[projectSlug]/edit/_components/ProjectForm.tsx index b9b1cdb9..368115d7 100644 --- a/src/app/(loggedInProjects)/[projectSlug]/edit/_components/ProjectForm.tsx +++ b/src/app/(loggedInProjects)/[projectSlug]/edit/_components/ProjectForm.tsx @@ -51,10 +51,10 @@ export const ProjectForm = ({ users, ...props }: Props) => { diff --git a/src/app/admin/projects/[projectSlug]/subsections/_components/SubsectionFelt.tsx b/src/app/admin/projects/[projectSlug]/subsections/_components/SubsectionPlacemarkImport.tsx similarity index 61% rename from src/app/admin/projects/[projectSlug]/subsections/_components/SubsectionFelt.tsx rename to src/app/admin/projects/[projectSlug]/subsections/_components/SubsectionPlacemarkImport.tsx index 14159531..3fd0f819 100644 --- a/src/app/admin/projects/[projectSlug]/subsections/_components/SubsectionFelt.tsx +++ b/src/app/admin/projects/[projectSlug]/subsections/_components/SubsectionPlacemarkImport.tsx @@ -3,7 +3,7 @@ import { blueButtonStyles, Link } from "@/src/core/components/links" import { quote } from "@/src/core/components/text/quote" import { useProjectSlug } from "@/src/core/routes/useProjectSlug" import getProject from "@/src/server/projects/queries/getProject" -import updateSubsectionsWithFeltData from "@/src/server/subsections/mutations/updateSubsectionsWithFeltData" +import updateSubsectionsWithPlacemark from "@/src/server/subsections/mutations/updateSubsectionsWithPlacemark" import getSubsections from "@/src/server/subsections/queries/getSubsections" import { useMutation, useQuery } from "@blitzjs/rpc" import { useRouter } from "next/navigation" @@ -11,19 +11,19 @@ import { useState } from "react" type Props = { project: Awaited> } -export const SubsectionFelt = ({ project }: Props) => { +export const SubsectionPlacemarkImport = ({ project }: Props) => { const [error, setError]: any | null = useState(null) const [isFetching, setIsFetching] = useState(false) const router = useRouter() const projectSlug = useProjectSlug() const [{ subsections }, { refetch }] = useQuery(getSubsections, { projectSlug }) - const [updateSubsectionMutation] = useMutation(updateSubsectionsWithFeltData) + const [updateSubsectionMutation] = useMutation(updateSubsectionsWithPlacemark) - const handleFeltDataClick = async () => { - if (!project.felt_subsection_geometry_source_url) { - window.alert("Keine Felt URL") - return console.error("No Felt URL") + const handlePlacemarkDataClick = async () => { + if (!project.placemarkUrl) { + window.alert("Keine Placemark URL") + return console.error("No Placemark URL") } setIsFetching(true) @@ -31,7 +31,7 @@ export const SubsectionFelt = ({ project }: Props) => { const subsectionIds = await updateSubsectionMutation( { subsections, - projectFeltUrl: project.felt_subsection_geometry_source_url, + projectPlacemarkUrl: project.placemarkUrl, }, { onSuccess: () => { @@ -41,8 +41,9 @@ export const SubsectionFelt = ({ project }: Props) => { }, }, ) - // @ts-expect-error ??? - router.push({ query: { updatedIds: subsectionIds?.join(",") } }) + router.push( + `/admin/projects/${projectSlug}/subsections?updatedIds=${subsectionIds?.join(",")}`, + ) } catch (error: any) { setError(error) return console.error(error) @@ -51,10 +52,14 @@ export const SubsectionFelt = ({ project }: Props) => { return (
- {project.felt_subsection_geometry_source_url ? ( + {project.placemarkUrl ? ( <> - {error && ( @@ -63,19 +68,19 @@ export const SubsectionFelt = ({ project }: Props) => {
{quote(error.toString())}
- Überprüfe die Felt-Url des Projekts. + Überprüfe die Placemark-Url des Projekts.
)} - - Geometrien in Felt bearbeiten + + Geometrien in Placemark bearbeiten ) : (
- - Um die Geometrien der Planungsabschnitte in Felt zu bearbeiten, hier die Felt-Url für - die Trasse angeben. + + Um die Geometrien der Planungsabschnitte in Placemark zu bearbeiten, hier die + Placemark-Url für die Trasse angeben.
)} diff --git a/src/app/admin/projects/[projectSlug]/subsections/_components/SubsectionTableAdmin.tsx b/src/app/admin/projects/[projectSlug]/subsections/_components/SubsectionTableAdmin.tsx index 90a641f2..0535f974 100644 --- a/src/app/admin/projects/[projectSlug]/subsections/_components/SubsectionTableAdmin.tsx +++ b/src/app/admin/projects/[projectSlug]/subsections/_components/SubsectionTableAdmin.tsx @@ -45,7 +45,7 @@ export const SubsectionTableAdmin = ({ subsections }: Props) => { {Boolean(updatedIds?.length) && (

Die Planungsabschnitte mit den Ids {JSON.stringify(updatedIds)} (in der - Tabelle blau hinterlegt) wurden in Felt erkannt und ggf. aktualisiert. + Tabelle blau hinterlegt) wurden in Placemark erkannt und ggf. aktualisiert.

)} @@ -146,6 +146,8 @@ export const SubsectionTableAdmin = ({ subsections }: Props) => { )} > {noPreviewForDefaultGeometry ? ( + "Geometrie unbekannt (Fallbackgeometrie)" + ) : ( { > Auf placemark.io öffnen - ) : ( - "unbekannt" )} diff --git a/src/app/admin/projects/[projectSlug]/subsections/page.tsx b/src/app/admin/projects/[projectSlug]/subsections/page.tsx index dbbaf6be..3761a7ab 100644 --- a/src/app/admin/projects/[projectSlug]/subsections/page.tsx +++ b/src/app/admin/projects/[projectSlug]/subsections/page.tsx @@ -5,7 +5,7 @@ import getProject from "@/src/server/projects/queries/getProject" import getSubsections from "@/src/server/subsections/queries/getSubsections" import { Metadata } from "next" import "server-only" -import { SubsectionFelt } from "./_components/SubsectionFelt" +import { SubsectionPlacemarkImport } from "./_components/SubsectionPlacemarkImport" import { SubsectionTableAdmin } from "./_components/SubsectionTableAdmin" export const metadata: Metadata = { title: "Planungsabschnitte" } @@ -29,7 +29,7 @@ export default async function AdminProjectSubsectionsPage({ /> - + diff --git a/src/core/components/forms/LabeledTextFieldCalculateLength.tsx b/src/core/components/forms/LabeledTextFieldCalculateLength.tsx index 4d93a252..8388bc63 100644 --- a/src/core/components/forms/LabeledTextFieldCalculateLength.tsx +++ b/src/core/components/forms/LabeledTextFieldCalculateLength.tsx @@ -25,7 +25,7 @@ export const LabeledTextFieldCalculateLength: React.FC = if (props.readOnly) { helpText = - "Dieser Wert wird aus den Geometrien (Felt) berechnet und kann nicht manuell editiert werden." + "Dieser Wert wird aus den Geometrien (Placemark) berechnet und kann nicht manuell editiert werden." } else if (!getValues("geometry")) { helpText = "Es ist keine Geometrie vorhanden" } else if (isPoint(getValues("geometry"))) { diff --git a/src/env.d.ts b/src/env.d.ts index 55fd410c..ca767225 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -10,8 +10,6 @@ namespace NodeJS { readonly S3_UPLOAD_KEY: string readonly S3_UPLOAD_SECRET: string - readonly FELT_TOKEN: `felt_pat_${string}` - readonly ADMIN_EMAIL: string readonly TS_API_KEY: string diff --git a/src/pages/[projectSlug]/abschnitte/[subsectionSlug]/edit.tsx b/src/pages/[projectSlug]/abschnitte/[subsectionSlug]/edit.tsx index e04ef35b..70a1c21d 100644 --- a/src/pages/[projectSlug]/abschnitte/[subsectionSlug]/edit.tsx +++ b/src/pages/[projectSlug]/abschnitte/[subsectionSlug]/edit.tsx @@ -77,7 +77,7 @@ const EditSubsection = () => { { { Mehrere Planungsabschnitte erstellen - Felt Import für Planungsabschnitte + Placemark Import für Planungsabschnitte diff --git a/src/pagesComponents/subsections/SubsectionForm.tsx b/src/pagesComponents/subsections/SubsectionForm.tsx index 838e1a90..4e0dae5e 100644 --- a/src/pagesComponents/subsections/SubsectionForm.tsx +++ b/src/pagesComponents/subsections/SubsectionForm.tsx @@ -24,11 +24,11 @@ import { z } from "zod" import { getPriorityTranslation } from "./utils/getPriorityTranslation" type Props> = FormProps & { - isFeltFieldsReadOnly?: boolean + isPlacemarkFieldsReadOnly?: boolean } function SubsectionFormWithQuery>({ - isFeltFieldsReadOnly, + isPlacemarkFieldsReadOnly, ...props }: Props) { const projectSlug = useProjectSlug() @@ -77,32 +77,22 @@ function SubsectionFormWithQuery>({ help="Die muss sicherstellen, dass die Geometrien in einer fortlaufenden Linie mit gleicher Linienrichtung dargestellt werden; sie ist auch die Standard-Sortierung." />
- - + +
diff --git a/src/server/projects/schema.ts b/src/server/projects/schema.ts index ede39a04..624a2ca9 100644 --- a/src/server/projects/schema.ts +++ b/src/server/projects/schema.ts @@ -7,7 +7,7 @@ export const ProjectSchema = z.object({ description: z.string().nullish(), logoSrc: z.string().nullish(), partnerLogoSrcs: z.array(z.string()).nullish(), - felt_subsection_geometry_source_url: z.union([z.string().url().nullish(), z.literal("")]), + placemarkUrl: z.union([z.string().url().nullish(), z.literal("")]), managerId: InputNumberOrNullSchema, }) diff --git a/src/server/subsections/mutations/updateSubsectionsWithFeltData.ts b/src/server/subsections/mutations/updateSubsectionsWithFeltData.ts deleted file mode 100644 index 80897cc7..00000000 --- a/src/server/subsections/mutations/updateSubsectionsWithFeltData.ts +++ /dev/null @@ -1,86 +0,0 @@ -import db from "@/db" -import { multilinestringToLinestring } from "@/src/pagesComponents/subsections/utils/multilinestringToLinestring" -import { resolver } from "@blitzjs/rpc" -import { length, lineString } from "@turf/turf" -import { z } from "zod" -import { FeltApiResponseSchema, SubsectionSchema } from "../schema" - -const UpdateSubsectionsWithFeltDataSchema = z.object({ - subsections: z.array( - SubsectionSchema.merge( - z.object({ - id: z.number(), - }), - ), - ), - projectFeltUrl: z.string().url({ message: "Ungültige Url." }).nullish(), -}) - -export default resolver.pipe( - resolver.zod(UpdateSubsectionsWithFeltDataSchema), - resolver.authorize("ADMIN"), - async ({ subsections, projectFeltUrl }) => { - const auth = `Bearer ${process.env.FELT_TOKEN}` - - const projectFeltUrlId = projectFeltUrl?.replace("https://felt.com/map/", "") - const url = `https://felt.com/api/v1/maps/${projectFeltUrlId}/elements` - - const updatedSeubsectionIds: number[] = [] - if (!projectFeltUrl) return null // todo - - const response = await fetch(url, { - method: "GET", - headers: { - Authorization: auth, - }, - }) - - const feltDataRaw = await response.json() - - const feltData = FeltApiResponseSchema.parse(feltDataRaw) - - const feltSubsections = feltData.data.features - - //iterate over ts-subsections - for (const tsSubsection of subsections) { - // check in felt-subsections for matching id - const matchingFeltSubsection = feltSubsections.find( - (s) => s?.properties["ts_pa_id"] === tsSubsection.slug, - ) - // if ts-subsection-slug matches one of the felt-subsection-id, update db subsection, fields: start, end, geometry - if (matchingFeltSubsection && matchingFeltSubsection.geometry.type === "MultiLineString") { - const updatedSubsection = await db.subsection.update({ - where: { id: tsSubsection.id }, - data: { - // Felt data is a multilinestring, but we need a linestring - // in Felt we get multiple lines, if we use the Route tool - // so we take the first linestring or we concat the multiple lines (in case last point of line a matches first point of line b) - geometry: matchingFeltSubsection.geometry - ? multilinestringToLinestring(matchingFeltSubsection?.geometry["coordinates"]) - : tsSubsection.geometry, - - start: matchingFeltSubsection.properties - ? matchingFeltSubsection.properties["ts_pa_start"] - : tsSubsection.start, - - end: matchingFeltSubsection.properties - ? matchingFeltSubsection.properties["ts_pa_end"] - : tsSubsection.end, - lengthKm: length( - lineString( - // @ts-expect-error - matchingFeltSubsection.geometry - ? multilinestringToLinestring(matchingFeltSubsection?.geometry["coordinates"]) - : tsSubsection.geometry, - ), - ), - }, - }) - - updatedSeubsectionIds.push(updatedSubsection.id) - } - } - - return updatedSeubsectionIds - }, -) diff --git a/src/server/subsections/mutations/updateSubsectionsWithPlacemark.ts b/src/server/subsections/mutations/updateSubsectionsWithPlacemark.ts new file mode 100644 index 00000000..2f910a5d --- /dev/null +++ b/src/server/subsections/mutations/updateSubsectionsWithPlacemark.ts @@ -0,0 +1,73 @@ +import db from "@/db" +import { multilinestringToLinestring } from "@/src/pagesComponents/subsections/utils/multilinestringToLinestring" +import { resolver } from "@blitzjs/rpc" +import { length, lineString } from "@turf/turf" +import { z } from "zod" +import { PlacemarkResponseSchema, SubsectionSchema } from "../schema" + +const updateSubsectionsWithPlacemarkSchema = z.object({ + subsections: z.array( + SubsectionSchema.merge( + z.object({ + id: z.number(), + }), + ), + ), + projectPlacemarkUrl: z.string().url({ message: "Ungültige Url." }).nullish(), +}) + +export default resolver.pipe( + resolver.zod(updateSubsectionsWithPlacemarkSchema), + resolver.authorize("ADMIN"), + // todo + async ({ subsections, projectPlacemarkUrl }) => { + const url = + projectPlacemarkUrl?.replace( + "https://placemark.fixmycity.de/map/", + "https://placemark.fixmycity.de/api/v1/map/", + ) + "/featurecollection" + + const updatedSubsectionIds: number[] = [] + if (!projectPlacemarkUrl) return null // todo + + const response = await fetch(url) + + const placemarkDataRaw = await response.json() + + const placemarkData = PlacemarkResponseSchema.parse(placemarkDataRaw) + + const placemarkSubsections = placemarkData.features + + //iterate over ts-subsections + for (const tsSubsection of subsections) { + // check in placemark-subsections for matching id + const matchingPlacemarkSubsection = placemarkSubsections.find( + (s) => s?.properties.subsectionSlug === tsSubsection.slug, + ) + const noSubsectionFound = !matchingPlacemarkSubsection ? tsSubsection.slug : null + console.log("noSubsectionFound for slug: ", noSubsectionFound) + // if ts-subsection-slug matches one of the placemark-subsection-id, update db subsection geometry + if (matchingPlacemarkSubsection) { + // placemark data can be a multilinestring, but we need a linestring + // so we take the first linestring or we concat the multiple lines (in case last point of line a matches first point of line b) + const newCoordinates = + matchingPlacemarkSubsection.geometry.type === "MultiLineString" + ? multilinestringToLinestring(matchingPlacemarkSubsection.geometry.coordinates) + : matchingPlacemarkSubsection.geometry.type === "LineString" + ? matchingPlacemarkSubsection.geometry.coordinates + : tsSubsection.geometry + const updatedSubsection = await db.subsection.update({ + where: { id: tsSubsection.id }, + data: { + geometry: newCoordinates, + // @ts-expect-error + lengthKm: length(lineString(newCoordinates)), + }, + }) + updatedSubsectionIds.push(updatedSubsection.id) + } + } + + return updatedSubsectionIds + }, +) diff --git a/src/server/subsections/schema.ts b/src/server/subsections/schema.ts index 040d9583..5764bc89 100644 --- a/src/server/subsections/schema.ts +++ b/src/server/subsections/schema.ts @@ -44,11 +44,6 @@ const FeatureSchema = z.object({ properties: PropertiesSchema, }) -export const FeltApiResponseSchema = z.object({ - data: z.object({ - type: z.string(), - metadata: z.record(z.any()), - features: z.array(FeatureSchema), - }), - links: z.any(), +export const PlacemarkResponseSchema = z.object({ + features: z.array(FeatureSchema), }) diff --git a/tests/blitz/mocks/mockProject.ts b/tests/blitz/mocks/mockProject.ts index 422740f6..a1ce6332 100644 --- a/tests/blitz/mocks/mockProject.ts +++ b/tests/blitz/mocks/mockProject.ts @@ -9,7 +9,7 @@ export const mockProject: Project = { Sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr.`, managerId: null, logoSrc: "rsv8-logo.png", - felt_subsection_geometry_source_url: null, + placemarkUrl: null, partnerLogoSrcs: ["rsv8-logo.png", "test.png"], createdAt: new Date(), updatedAt: new Date(), From c475f037c99f2baa125f89ca9ce704065913466a Mon Sep 17 00:00:00 2001 From: JohannaPeanut <76495099+JohannaPeanut@users.noreply.github.com> Date: Tue, 4 Feb 2025 12:17:21 +0100 Subject: [PATCH 20/32] Project: update placeholder/hint in form --- .../[projectSlug]/edit/_components/ProjectForm.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/(loggedInProjects)/[projectSlug]/edit/_components/ProjectForm.tsx b/src/app/(loggedInProjects)/[projectSlug]/edit/_components/ProjectForm.tsx index 368115d7..edb22f3e 100644 --- a/src/app/(loggedInProjects)/[projectSlug]/edit/_components/ProjectForm.tsx +++ b/src/app/(loggedInProjects)/[projectSlug]/edit/_components/ProjectForm.tsx @@ -53,8 +53,8 @@ export const ProjectForm = ({ users, ...props }: Props) => { type="text" name="placemarkUrl" label="Placemark Url" - placeholder="https://places.nudafa.de/map/beispiel-karte-id" - help="Die Placemark-Karte liegt in Fixmycity's Placemark. Hier die Url der nicht öffentlichen Karte (https://places.nudafa.de/map/...) angeben." + placeholder="https://placemark.fixmycity.de/map/beispiel-karte-id" + help="Die Placemark-Karte liegt in Fixmycity's Placemark. Hier die Url der nicht öffentlichen Karte (https://placemark.fixmycity.de/map/...) angeben." /> From 643c135560138c06b5fc3762d5ab5fa049cec285 Mon Sep 17 00:00:00 2001 From: JohannaPeanut <76495099+JohannaPeanut@users.noreply.github.com> Date: Tue, 4 Feb 2025 15:54:59 +0100 Subject: [PATCH 21/32] Subsection: update column estimatedCompletionDateString --- .../migration.sql | 9 ++++++ db/schema.prisma | 28 +++++++++---------- src/app/api/projects/[slug]/route.ts | 4 +-- .../abschnitte/[subsectionSlug]/edit.tsx | 11 ++------ src/pages/[projectSlug]/abschnitte/new.tsx | 3 -- .../subsections/SubsectionForm.tsx | 7 +++-- src/server/subsections/schema.ts | 13 +++------ 7 files changed, 35 insertions(+), 40 deletions(-) create mode 100644 db/migrations/20250204141919_project_rename_column_estimated_completion_date_string/migration.sql diff --git a/db/migrations/20250204141919_project_rename_column_estimated_completion_date_string/migration.sql b/db/migrations/20250204141919_project_rename_column_estimated_completion_date_string/migration.sql new file mode 100644 index 00000000..2035caa1 --- /dev/null +++ b/db/migrations/20250204141919_project_rename_column_estimated_completion_date_string/migration.sql @@ -0,0 +1,9 @@ +/* + Warnings: + + - You are about to drop the column `estimatedCompletionDate` on the `Subsection` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Subsection" DROP COLUMN "estimatedCompletionDate", +ADD COLUMN "estimatedCompletionDateString" TEXT; diff --git a/db/schema.prisma b/db/schema.prisma index 3970678e..f351388f 100644 --- a/db/schema.prisma +++ b/db/schema.prisma @@ -239,20 +239,20 @@ model Operator { } model Subsection { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - // - slug String // shortTitle and URL Slug - start String - end String - order Int @default(autoincrement()) - geometry Json - labelPos LabelPositionEnum @default(top) - description String? - lengthKm Float // Kilometer - priority PriorityEnum @default(OPEN) // Priorität - estimatedCompletionDate DateTime? // Jahresquartal der geplanten Fertigstellung als string Form "1-2023" + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + // + slug String // shortTitle and URL Slug + start String + end String + order Int @default(autoincrement()) + geometry Json + labelPos LabelPositionEnum @default(top) + description String? + lengthKm Float // Kilometer + priority PriorityEnum @default(OPEN) // Priorität + estimatedCompletionDateString String? // Jahresquartal der geplanten Fertigstellung als string Form "1-2023" // project Project @relation(fields: [projectId], references: [id]) diff --git a/src/app/api/projects/[slug]/route.ts b/src/app/api/projects/[slug]/route.ts index 4c8f627a..590e43ad 100644 --- a/src/app/api/projects/[slug]/route.ts +++ b/src/app/api/projects/[slug]/route.ts @@ -32,7 +32,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ slug geometry: true, // do we want to have the slug here as well? operator: { select: { title: true } }, - estimatedCompletionDate: true, + estimatedCompletionDateString: true, SubsubsectionStatus: { select: { slug: true, title: true } }, }, }) @@ -43,7 +43,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ slug subsectionSlug: s.slug, projectSlug: slug, operator: s.operator?.title, - estimatedCompletionDate: s.estimatedCompletionDate, + estimatedCompletionDateString: s.estimatedCompletionDateString, status: s.SubsubsectionStatus?.title, }), ), diff --git a/src/pages/[projectSlug]/abschnitte/[subsectionSlug]/edit.tsx b/src/pages/[projectSlug]/abschnitte/[subsectionSlug]/edit.tsx index aeda6b39..3d937175 100644 --- a/src/pages/[projectSlug]/abschnitte/[subsectionSlug]/edit.tsx +++ b/src/pages/[projectSlug]/abschnitte/[subsectionSlug]/edit.tsx @@ -7,13 +7,12 @@ import { seoEditTitleSlug, shortTitle } from "@/src/core/components/text" import { LayoutRs, MetaTags } from "@/src/core/layouts" import { useProjectSlug } from "@/src/core/routes/usePagesDirectoryProjectSlug" import { useSlug } from "@/src/core/routes/usePagesDirectorySlug" -import { getDate } from "@/src/pagesComponents/calendar-entries/utils/splitStartAt" import { SubsectionForm } from "@/src/pagesComponents/subsections/SubsectionForm" import getProject from "@/src/server/projects/queries/getProject" import deleteSubsection from "@/src/server/subsections/mutations/deleteSubsection" import updateSubsection from "@/src/server/subsections/mutations/updateSubsection" import getSubsection from "@/src/server/subsections/queries/getSubsection" -import { SubsectionFormSchema } from "@/src/server/subsections/schema" +import { SubsectionSchema } from "@/src/server/subsections/schema" import { BlitzPage, Routes } from "@blitzjs/next" import { useMutation, useQuery } from "@blitzjs/rpc" import { clsx } from "clsx" @@ -39,9 +38,6 @@ const EditSubsection = () => { id: subsection.id, slug: `pa${values.slug}`, projectSlug, - estimatedCompletionDate: values.estimatedCompletionDate - ? new Date(values.estimatedCompletionDate) - : null, }) await setQueryData(updated) await router.push( @@ -84,13 +80,10 @@ const EditSubsection = () => { isFeltFieldsReadOnly={Boolean(project?.felt_subsection_geometry_source_url)} className="mt-10" submitText="Speichern" - schema={SubsectionFormSchema} + schema={SubsectionSchema} initialValues={{ ...subsection, slug: subsection.slug.replace(/^pa/, ""), - estimatedCompletionDate: subsection.estimatedCompletionDate - ? getDate(subsection.estimatedCompletionDate) - : "", }} onSubmit={handleSubmit} /> diff --git a/src/pages/[projectSlug]/abschnitte/new.tsx b/src/pages/[projectSlug]/abschnitte/new.tsx index f541f67f..3b0455b2 100644 --- a/src/pages/[projectSlug]/abschnitte/new.tsx +++ b/src/pages/[projectSlug]/abschnitte/new.tsx @@ -32,9 +32,6 @@ const NewSubsection = () => { slug: `pa${values.slug}`, projectId: project.id, projectSlug, - estimatedCompletionDate: values.estimatedCompletionDate - ? new Date(values.estimatedCompletionDate) - : null, }) await router.push( Routes.SubsectionDashboardPage({ diff --git a/src/pagesComponents/subsections/SubsectionForm.tsx b/src/pagesComponents/subsections/SubsectionForm.tsx index 82dc46db..b3de2b0c 100644 --- a/src/pagesComponents/subsections/SubsectionForm.tsx +++ b/src/pagesComponents/subsections/SubsectionForm.tsx @@ -146,9 +146,10 @@ function SubsectionFormWithQuery>({ options={getUserSelectOptions(users)} /> diff --git a/src/server/subsections/schema.ts b/src/server/subsections/schema.ts index e67fdfda..bde089ac 100644 --- a/src/server/subsections/schema.ts +++ b/src/server/subsections/schema.ts @@ -1,7 +1,6 @@ import { InputNumberOrNullSchema, InputNumberSchema, SlugSchema } from "@/src/core/utils" import { LabelPositionEnum } from "@prisma/client" import { z } from "zod" -import { NullableDateSchema, NullableDateSchemaForm } from "../subsubsections/schema" export const SubsectionSchema = z.object({ slug: SlugSchema, @@ -17,15 +16,11 @@ export const SubsectionSchema = z.object({ operatorId: InputNumberOrNullSchema, networkHierarchyId: InputNumberOrNullSchema, subsubsectionStatusId: InputNumberOrNullSchema, - estimatedCompletionDate: NullableDateSchema, + estimatedCompletionDateString: z + .string() + .regex(/^\d{4}-\d{2}$/, { message: "Datum im Format JJJJ-MM" }) + .nullish(), }) -export const SubsectionFormSchema = SubsectionSchema.omit({ - estimatedCompletionDate: true, -}).merge( - z.object({ - estimatedCompletionDate: NullableDateSchemaForm, - }), -) export const SubsectionsFormSchema = z.object({ prefix: z.string().regex(/^[a-z0-9-.]*$/, { From cdb4b0c6b55fecf0a7856d818caa7d0a38d69d4f Mon Sep 17 00:00:00 2001 From: JohannaPeanut <76495099+JohannaPeanut@users.noreply.github.com> Date: Tue, 4 Feb 2025 16:00:04 +0100 Subject: [PATCH 22/32] Update schema.prisma --- db/schema.prisma | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/db/schema.prisma b/db/schema.prisma index f351388f..8feb68ae 100644 --- a/db/schema.prisma +++ b/db/schema.prisma @@ -124,20 +124,20 @@ model Token { // } model Project { - id Int @id @default(autoincrement()) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - // - slug String @unique // shortTitle and URL Slug - subTitle String? - description String? - logoSrc String? - partnerLogoSrcs String[] - felt_subsection_geometry_source_url String? - exportEnabled Boolean @default(false) - // - manager User? @relation(fields: [managerId], references: [id]) - managerId Int? + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + // + slug String @unique // shortTitle and URL Slug + subTitle String? + description String? + logoSrc String? + partnerLogoSrcs String[] + placemarkUrl String? + exportEnabled Boolean @default(false) + // + manager User? @relation(fields: [managerId], references: [id]) + managerId Int? surveyResponseTopics SurveyResponseTopic[] subsections Subsection[] From 7980f94be20aba007418b8a2315bf3e5f55eeac5 Mon Sep 17 00:00:00 2001 From: JohannaPeanut <76495099+JohannaPeanut@users.noreply.github.com> Date: Wed, 5 Feb 2025 10:33:43 +0100 Subject: [PATCH 23/32] Project: add cors headers for export --- src/app/api/projects/[slug]/route.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/app/api/projects/[slug]/route.ts b/src/app/api/projects/[slug]/route.ts index 590e43ad..786db614 100644 --- a/src/app/api/projects/[slug]/route.ts +++ b/src/app/api/projects/[slug]/route.ts @@ -1,6 +1,13 @@ import db from "@/db" import { featureCollection, lineString } from "@turf/helpers" +const corsHeaders = { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", + "Access-Control-Allow-Headers": "Content-Type, Authorization", +} + export async function GET(request: Request, { params }: { params: Promise<{ slug: string }> }) { try { const slug = (await params).slug @@ -15,13 +22,14 @@ export async function GET(request: Request, { params }: { params: Promise<{ slug console.log("No project found for slug: ", slug) return new Response(JSON.stringify({ error: "No project found for that slug" }), { status: 404, + headers: corsHeaders, }) } if (!project.exportEnabled) { console.log("Export is disabled for project with slug: ", slug) return new Response( JSON.stringify({ error: "The export for this project is disabled by the admin" }), - { status: 404 }, + { status: 404, headers: corsHeaders }, ) } @@ -50,10 +58,13 @@ export async function GET(request: Request, { params }: { params: Promise<{ slug ) return new Response(JSON.stringify(projectFeatures), { - headers: { "Content-Type": "application/json" }, + headers: corsHeaders, }) } catch (error) { console.error("Error fetching subsections:", error) - return new Response(JSON.stringify({ error: "Internal Server Error" }), { status: 500 }) + return new Response(JSON.stringify({ error: "Internal Server Error" }), { + status: 500, + headers: corsHeaders, + }) } } From 3f70768efc5fb2e7a3c3de38078acdfba8577385 Mon Sep 17 00:00:00 2001 From: JohannaPeanut <76495099+JohannaPeanut@users.noreply.github.com> Date: Wed, 5 Feb 2025 11:56:19 +0100 Subject: [PATCH 24/32] Project: accept .json ending in export --- src/app/api/projects/[slug]/route.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/app/api/projects/[slug]/route.ts b/src/app/api/projects/[slug]/route.ts index 786db614..928c5fc0 100644 --- a/src/app/api/projects/[slug]/route.ts +++ b/src/app/api/projects/[slug]/route.ts @@ -10,7 +10,12 @@ const corsHeaders = { export async function GET(request: Request, { params }: { params: Promise<{ slug: string }> }) { try { - const slug = (await params).slug + let slug = (await params).slug + + if (slug.endsWith(".json")) { + slug = slug.replace(/\.json$/, "") + } + const project = await db.project.findFirst({ where: { slug }, select: { From dd093cb6e9f94afe215acc489f5dd9f2cadf91ba Mon Sep 17 00:00:00 2001 From: JohannaPeanut <76495099+JohannaPeanut@users.noreply.github.com> Date: Wed, 12 Feb 2025 10:12:37 +0100 Subject: [PATCH 25/32] Breadcrumb: update Link --- src/app/admin/_components/Breadcrumb.tsx | 5 +++-- .../subsections/multiple-new/page.tsx | 14 ++++++++++---- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/app/admin/_components/Breadcrumb.tsx b/src/app/admin/_components/Breadcrumb.tsx index a058558d..f561e5fb 100644 --- a/src/app/admin/_components/Breadcrumb.tsx +++ b/src/app/admin/_components/Breadcrumb.tsx @@ -2,10 +2,11 @@ import { HomeIcon } from "@heroicons/react/20/solid" import { clsx } from "clsx" import { Route } from "next" -import Link from "next/link" +import Link from "next/dist/client/link" + import { usePathname } from "next/navigation" -export type TBreadcrumb = { name: string; href?: Route } +export type TBreadcrumb = { name: string; href?: Route | string } type Props = { pages: TBreadcrumb[] } export const Breadcrumb = ({ pages }: Props) => { diff --git a/src/app/admin/projects/[projectSlug]/subsections/multiple-new/page.tsx b/src/app/admin/projects/[projectSlug]/subsections/multiple-new/page.tsx index b2bed1f5..59f4eb19 100644 --- a/src/app/admin/projects/[projectSlug]/subsections/multiple-new/page.tsx +++ b/src/app/admin/projects/[projectSlug]/subsections/multiple-new/page.tsx @@ -5,16 +5,22 @@ import "server-only" import { MultipleNewSubsectionsForm } from "./_components/MultipleNewSubsectionsForm" export const metadata: Metadata = { title: "Planungsabschnitte im Bulk-Mode hinzufügen" } - -export default function AdminProjectSubsectionMultipleNewPage() { +export default async function AdminProjectSubsectionMultipleNewPage({ + params: { projectSlug }, +}: { + params: { projectSlug: string } +}) { return ( <> From eb6e6153186dc0636d374d6bc7fcedad81295f07 Mon Sep 17 00:00:00 2001 From: JohannaPeanut <76495099+JohannaPeanut@users.noreply.github.com> Date: Wed, 12 Feb 2025 10:14:49 +0100 Subject: [PATCH 26/32] Project: use Placemark Play for subsections update --- .../migration.sql | 8 + db/schema.prisma | 1 - db/seeds/projects.ts | 4 - .../edit/_components/ProjectForm.tsx | 8 - .../_components/SubsectionPlacemarkImport.tsx | 211 +++++++++++++----- .../_components/SubsectionTableAdmin.tsx | 7 +- .../[projectSlug]/subsections/edit/page.tsx | 35 +++ .../[projectSlug]/subsections/page.tsx | 15 +- .../abschnitte/[subsectionSlug]/edit.tsx | 1 - src/pages/[projectSlug]/abschnitte/new.tsx | 1 - .../subsections/SubsectionForm.tsx | 24 +- src/server/projects/schema.ts | 1 - .../updateSubsectionsWithPlacemark.ts | 34 +-- src/server/subsections/schema.ts | 23 +- tests/blitz/mocks/mockProject.ts | 1 - 15 files changed, 248 insertions(+), 126 deletions(-) create mode 100644 db/migrations/20250211112830_project_drop_placemarkurl/migration.sql create mode 100644 src/app/admin/projects/[projectSlug]/subsections/edit/page.tsx diff --git a/db/migrations/20250211112830_project_drop_placemarkurl/migration.sql b/db/migrations/20250211112830_project_drop_placemarkurl/migration.sql new file mode 100644 index 00000000..236c8f65 --- /dev/null +++ b/db/migrations/20250211112830_project_drop_placemarkurl/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - You are about to drop the column `placemarkUrl` on the `Project` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Project" DROP COLUMN "placemarkUrl"; diff --git a/db/schema.prisma b/db/schema.prisma index 8feb68ae..303d6465 100644 --- a/db/schema.prisma +++ b/db/schema.prisma @@ -133,7 +133,6 @@ model Project { description String? logoSrc String? partnerLogoSrcs String[] - placemarkUrl String? exportEnabled Boolean @default(false) // manager User? @relation(fields: [managerId], references: [id]) diff --git a/db/seeds/projects.ts b/db/seeds/projects.ts index fe589059..e34bb84a 100644 --- a/db/seeds/projects.ts +++ b/db/seeds/projects.ts @@ -10,7 +10,6 @@ const seedProjects = async () => { Sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr.`, managerId: null, logoSrc: "rsv8-logo.png", - placemarkUrl: null, partnerLogoSrcs: ["rsv8-logo.png", "test.png"], exportEnabled: false, }, @@ -20,7 +19,6 @@ Sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, description: null, managerId: null, logoSrc: null, - placemarkUrl: null, partnerLogoSrcs: [], exportEnabled: false, }, @@ -30,7 +28,6 @@ Sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, description: null, managerId: null, logoSrc: null, - placemarkUrl: null, partnerLogoSrcs: [], exportEnabled: false, }, @@ -40,7 +37,6 @@ Sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, description: null, managerId: null, logoSrc: null, - placemarkUrl: null, partnerLogoSrcs: [], exportEnabled: false, }, diff --git a/src/app/(loggedInProjects)/[projectSlug]/edit/_components/ProjectForm.tsx b/src/app/(loggedInProjects)/[projectSlug]/edit/_components/ProjectForm.tsx index 73d3d41b..eb593ccb 100644 --- a/src/app/(loggedInProjects)/[projectSlug]/edit/_components/ProjectForm.tsx +++ b/src/app/(loggedInProjects)/[projectSlug]/edit/_components/ProjectForm.tsx @@ -49,14 +49,6 @@ export const ProjectForm = ({ users, ...props }: Props) => { , von NextJS intern optimiert und hier referenziert.

- {/* @ts-expect-error the defaults work fine; but the helper should be updated at some point */} diff --git a/src/app/admin/projects/[projectSlug]/subsections/_components/SubsectionPlacemarkImport.tsx b/src/app/admin/projects/[projectSlug]/subsections/_components/SubsectionPlacemarkImport.tsx index 3fd0f819..d8660684 100644 --- a/src/app/admin/projects/[projectSlug]/subsections/_components/SubsectionPlacemarkImport.tsx +++ b/src/app/admin/projects/[projectSlug]/subsections/_components/SubsectionPlacemarkImport.tsx @@ -1,89 +1,190 @@ "use client" -import { blueButtonStyles, Link } from "@/src/core/components/links" -import { quote } from "@/src/core/components/text/quote" +import { SuperAdminBox } from "@/src/core/components/AdminBox" +import { blueButtonStyles, Link, selectLinkStyle } from "@/src/core/components/links" +import { ButtonWrapper } from "@/src/core/components/links/ButtonWrapper" import { useProjectSlug } from "@/src/core/routes/useProjectSlug" +import { isProduction } from "@/src/core/utils" import getProject from "@/src/server/projects/queries/getProject" import updateSubsectionsWithPlacemark from "@/src/server/subsections/mutations/updateSubsectionsWithPlacemark" import getSubsections from "@/src/server/subsections/queries/getSubsections" +import { FeatureCollectionSchema } from "@/src/server/subsections/schema" import { useMutation, useQuery } from "@blitzjs/rpc" +import clsx from "clsx" import { useRouter } from "next/navigation" import { useState } from "react" +import { z } from "zod" type Props = { project: Awaited> } export const SubsectionPlacemarkImport = ({ project }: Props) => { - const [error, setError]: any | null = useState(null) - const [isFetching, setIsFetching] = useState(false) - + type TFeatureCollectionSchema = z.infer + type FileUploadState = "INITIAL" | "FILE_SELECTED" + const [fileState, setFileState] = useState("INITIAL") + const [selectedFile, setSelectedFile] = useState(null) + const [fileContent, setFileContent] = useState(null) const router = useRouter() const projectSlug = useProjectSlug() - const [{ subsections }, { refetch }] = useQuery(getSubsections, { projectSlug }) + const [{ subsections }] = useQuery(getSubsections, { projectSlug }) const [updateSubsectionMutation] = useMutation(updateSubsectionsWithPlacemark) - const handlePlacemarkDataClick = async () => { - if (!project.placemarkUrl) { - window.alert("Keine Placemark URL") - return console.error("No Placemark URL") - } - setIsFetching(true) - + const handleUpdateDataClick = async () => { + if (!fileContent) return try { - const subsectionIds = await updateSubsectionMutation( - { - subsections, - projectPlacemarkUrl: project.placemarkUrl, - }, - { - onSuccess: () => { - void refetch() - setIsFetching(false) - setError(null) - }, - }, - ) + const subsectionIds = await updateSubsectionMutation({ + subsections, + newGeometry: fileContent, + }) router.push( `/admin/projects/${projectSlug}/subsections?updatedIds=${subsectionIds?.join(",")}`, ) } catch (error: any) { - setError(error) + console.log("error") return console.error(error) } } + const placemarkPlayUrl = `https://play.placemark.io/?load=https://${!isProduction && "staging."}trassenscout.de/api/projects/${projectSlug}.json` + + const handleUploadChange = async (event: React.ChangeEvent) => { + const files = event.target.files as FileList + const file = files[0] + if (file) { + try { + const content = await file.text() + const parsedContent = JSON.parse(content) + console.log("Parsed Object:", parsedContent) + const geoJson = FeatureCollectionSchema.parse(parsedContent) + setFileContent(geoJson) + setSelectedFile(file) + setFileState("FILE_SELECTED") + } catch (error) { + console.error("Error parsing file:", error) + alert("Invalid GeoJSON file.") + } + } else { + setSelectedFile(null) + setFileContent(null) + setFileState("INITIAL") + } + } + return (
- {project.placemarkUrl ? ( - <> - - - {error && ( -
- Es ist ein Fehler aufgetreten: -
- {quote(error.toString())} -
- Überprüfe die Placemark-Url des Projekts. -
- )} +
+

Prozess zum Aktualisieren der Geometrien der Planungsabschnitte eines Projekts:

+
    +
  1. + Nur für neue Planungsabschnitte:{" "} + + Hier + {" "} + mehrere Planungsabschnitte gleichzeitig anlegen. Die Geometrien der neuen + Planungsabschnitte werden mit einer Default-Geometrie angelegt. +
  2. +
  3. + Export des Projekts{" "} + + hier + {" "} + einschalten. +
  4. +
  5. + Der Button unten `Geometrien in Placemark Play bearbeiten` öffnet alle Geometrien der + Planungsabschnitte dieses Projekts in Placemark Play. Dort Geometrien bearbeiten. Alle + neu angelegten Planungsabschnitte sind an ihrer Default-Geometrie zu erkennen. Diese + bearbeiten oder löschen und neu anlegen - für den Re-import{" "} + + wichtig: die property `subsectionSlug` der Geometrie in Placemark Play muss mit dem + `slug` des Planungsabschnitts im TS übereinstimmen. + {" "} + + Hier + {" "} + in der Übersicht, kann der `slug` des Planungsabschnitts eingesehen und kopiert werden. +
  6. +
  7. + Die bearbeiteten Geometrien in Placemark Play `File` - `Export` als GeoJSON speichern. +
  8. +
  9. + Die gespeicherte GeoJSON-Datei hier über `Datei auswählen` auswählen und auf `Geometrien + der Planungsabschnitte ersetzen` klicken, um die Geometrien der Planungsabschnitte zu + aktualisieren.{" "} + + Wichtig: Die Geometrien der Planungsabschnitte im TS werden durch die Geometrien in + der hochgeladenen Datei ersetzt. + {" "} + Dies passiert lediglich für die Planungsabshcnitte, für die in der Datei ein Feature mit + dem übereinstimmenden Slug gefunden wird. +
  10. +
  11. + In der{" "} + + Übersicht der Planungsabschnitte + {" "} + sind die aktualisierten Planungsabschnitte danach blau hinterlegt. +
  12. +
+
- - Geometrien in Placemark bearbeiten - - + {project.exportEnabled ? ( + + Geometrien in Placemark Play bearbeiten + ) : ( -
- - Um die Geometrien der Planungsabschnitte in Placemark zu bearbeiten, hier die - Placemark-Url für die Trasse angeben. - +
+ Um mehrere Geometrien gleichzeitig in Placemark Play zu bearbeiten, muss der Geometrie + Export des Projekts hier eingeschaltet werden.
)} +
+ {["FILE_SELECTED"].includes(fileState) && ( +
+ Ausgewählte Datei: {selectedFile!.name} +
+ )} + + {["INITIAL", "FILE_SELECTED"].includes(fileState) && ( +
+ + +
+ )} + {["FILE_SELECTED"].includes(fileState) && ( + + )} +
+ + +

+ state: {fileState} +

+ {["FILE_SELECTED"].includes(fileState) && ( +

+ type: {selectedFile!.type} +
+ size: {selectedFile!.size} +
+

+ )} +
+
) } diff --git a/src/app/admin/projects/[projectSlug]/subsections/_components/SubsectionTableAdmin.tsx b/src/app/admin/projects/[projectSlug]/subsections/_components/SubsectionTableAdmin.tsx index 0535f974..efd065ab 100644 --- a/src/app/admin/projects/[projectSlug]/subsections/_components/SubsectionTableAdmin.tsx +++ b/src/app/admin/projects/[projectSlug]/subsections/_components/SubsectionTableAdmin.tsx @@ -39,13 +39,14 @@ export const SubsectionTableAdmin = ({ subsections }: Props) => { } } } - return (
{Boolean(updatedIds?.length) && (

- Die Planungsabschnitte mit den Ids {JSON.stringify(updatedIds)} (in der - Tabelle blau hinterlegt) wurden in Placemark erkannt und ggf. aktualisiert. + {updatedIds![0] === "" + ? "Keine Planungsabschnitte im ausgewählten GeoJSON erkannt." + : `Die Planungsabschnitte mit den Ids ${JSON.stringify(updatedIds)} (in der + Tabelle blau hinterlegt) wurden im ausgewählten GeoJSON erkannt und ggf. aktualisiert.`}

)} diff --git a/src/app/admin/projects/[projectSlug]/subsections/edit/page.tsx b/src/app/admin/projects/[projectSlug]/subsections/edit/page.tsx new file mode 100644 index 00000000..f8b4349c --- /dev/null +++ b/src/app/admin/projects/[projectSlug]/subsections/edit/page.tsx @@ -0,0 +1,35 @@ +import { Breadcrumb } from "@/src/app/admin/_components/Breadcrumb" +import { HeaderWrapper } from "@/src/app/admin/_components/HeaderWrapper" +import { invoke } from "@/src/blitz-server" +import getProject from "@/src/server/projects/queries/getProject" +import { Metadata } from "next" +import "server-only" +import { SubsectionPlacemarkImport } from "../_components/SubsectionPlacemarkImport" + +export const metadata: Metadata = { title: "Planungsabschnitte bearbeiten" } + +export default async function AdminProjectSubsectionsEditPage({ + params: { projectSlug }, +}: { + params: { projectSlug: string } +}) { + const project = await invoke(getProject, { projectSlug }) + return ( + <> + + + + + + ) +} diff --git a/src/app/admin/projects/[projectSlug]/subsections/page.tsx b/src/app/admin/projects/[projectSlug]/subsections/page.tsx index 3761a7ab..84726af1 100644 --- a/src/app/admin/projects/[projectSlug]/subsections/page.tsx +++ b/src/app/admin/projects/[projectSlug]/subsections/page.tsx @@ -1,11 +1,11 @@ import { Breadcrumb } from "@/src/app/admin/_components/Breadcrumb" import { HeaderWrapper } from "@/src/app/admin/_components/HeaderWrapper" import { invoke } from "@/src/blitz-server" +import { Link } from "@/src/core/components/links" import getProject from "@/src/server/projects/queries/getProject" import getSubsections from "@/src/server/subsections/queries/getSubsections" import { Metadata } from "next" import "server-only" -import { SubsectionPlacemarkImport } from "./_components/SubsectionPlacemarkImport" import { SubsectionTableAdmin } from "./_components/SubsectionTableAdmin" export const metadata: Metadata = { title: "Planungsabschnitte" } @@ -28,9 +28,16 @@ export default async function AdminProjectSubsectionsPage({ ]} /> - - - + {project.exportEnabled ? ( + + Geometrien bearbeiten + + ) : ( +
+ Um mehrere Geometrien gleichzeitig in Placemark Play zu bearbeiten, muss der Geometrie + Export des Projekts hier eingeschaltet werden. +
+ )} ) diff --git a/src/pages/[projectSlug]/abschnitte/[subsectionSlug]/edit.tsx b/src/pages/[projectSlug]/abschnitte/[subsectionSlug]/edit.tsx index 75cf09de..a3d69f64 100644 --- a/src/pages/[projectSlug]/abschnitte/[subsectionSlug]/edit.tsx +++ b/src/pages/[projectSlug]/abschnitte/[subsectionSlug]/edit.tsx @@ -77,7 +77,6 @@ const EditSubsection = () => { { > = FormProps & { - isPlacemarkFieldsReadOnly?: boolean -} +type Props> = FormProps -function SubsectionFormWithQuery>({ - isPlacemarkFieldsReadOnly, - ...props -}: Props) { +function SubsectionFormWithQuery>({ ...props }: Props) { const projectSlug = useProjectSlug() const [users] = useQuery(getProjectUsers, { projectSlug, role: "EDITOR" }) const [{ operators }] = useQuery(getOperatorsWithCount, { projectSlug }) @@ -88,19 +83,8 @@ function SubsectionFormWithQuery>({
- - + +
{ - const url = - projectPlacemarkUrl?.replace( - "https://placemark.fixmycity.de/map/", - "https://placemark.fixmycity.de/api/v1/map/", - ) + "/featurecollection" - + async ({ subsections, newGeometry }) => { const updatedSubsectionIds: number[] = [] - if (!projectPlacemarkUrl) return null // todo - - const response = await fetch(url) - - const placemarkDataRaw = await response.json() - - const placemarkData = PlacemarkResponseSchema.parse(placemarkDataRaw) - - const placemarkSubsections = placemarkData.features + const placemarkSubsections = newGeometry.features //iterate over ts-subsections for (const tsSubsection of subsections) { @@ -52,10 +37,12 @@ export default resolver.pipe( // so we take the first linestring or we concat the multiple lines (in case last point of line a matches first point of line b) const newCoordinates = matchingPlacemarkSubsection.geometry.type === "MultiLineString" - ? multilinestringToLinestring(matchingPlacemarkSubsection.geometry.coordinates) + ? // @ts-expect-error + multilinestringToLinestring(matchingPlacemarkSubsection.geometry.coordinates) : matchingPlacemarkSubsection.geometry.type === "LineString" ? matchingPlacemarkSubsection.geometry.coordinates - : tsSubsection.geometry + : // if geometry type of matching placemark subsection is not LineString or MultiLineString, we skip this subsection + tsSubsection.geometry const updatedSubsection = await db.subsection.update({ where: { id: tsSubsection.id }, data: { @@ -64,10 +51,13 @@ export default resolver.pipe( lengthKm: length(lineString(newCoordinates)), }, }) + updatedSubsectionIds.push(updatedSubsection.id) } } - + if (updatedSubsectionIds.length === 0) { + console.log("No subsections found for placemark data") + } return updatedSubsectionIds }, ) diff --git a/src/server/subsections/schema.ts b/src/server/subsections/schema.ts index baa4a396..7c24dacb 100644 --- a/src/server/subsections/schema.ts +++ b/src/server/subsections/schema.ts @@ -28,6 +28,12 @@ export const SubsectionsFormSchema = z.object({ }), no: z.coerce.number(), }) +const PointSchema = z.tuple([z.number(), z.number()]) // [x, y] +const MultiPointSchema = z.array(PointSchema) // [[x1, y1], [x2, y2], ...] +const LineStringSchema = z.array(PointSchema) // [[x1, y1], [x2, y2], ...] +const MultiLineStringSchema = z.array(LineStringSchema) // [[[x1, y1], [x2, y2]], [[x3, y3], [x4, y4]]] +const PolygonSchema = z.array(LineStringSchema) // Outer ring and inner rings: [[[x1, y1], [x2, y2]], [[x3, y3], [x4, y4]]] +const MultiPolygonSchema = z.array(PolygonSchema) // Collection of polygons: [[[[x1, y1], [x2, y2]]], [[[x3, y3], [x4, y4]]]] const GeometrySchema = z.object({ type: z.union([ @@ -39,16 +45,23 @@ const GeometrySchema = z.object({ z.literal("MultiPolygon"), z.literal("GeometryCollection"), ]), - coordinates: z.array(z.any()), + coordinates: z.union([ + PointSchema, + MultiPointSchema, + LineStringSchema, + MultiLineStringSchema, + PolygonSchema, + MultiPolygonSchema, + ]), }) -const PropertiesSchema = z.record(z.any()) - const FeatureSchema = z.object({ + type: z.literal("Feature"), + properties: z.record(z.any()), geometry: GeometrySchema, - properties: PropertiesSchema, }) -export const PlacemarkResponseSchema = z.object({ +export const FeatureCollectionSchema = z.object({ + type: z.literal("FeatureCollection"), features: z.array(FeatureSchema), }) diff --git a/tests/blitz/mocks/mockProject.ts b/tests/blitz/mocks/mockProject.ts index 6d0e7463..3d05e9b9 100644 --- a/tests/blitz/mocks/mockProject.ts +++ b/tests/blitz/mocks/mockProject.ts @@ -9,7 +9,6 @@ export const mockProject: Project = { Sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr.`, managerId: null, logoSrc: "rsv8-logo.png", - placemarkUrl: null, partnerLogoSrcs: ["rsv8-logo.png", "test.png"], createdAt: new Date(), updatedAt: new Date(), From bed677763010bff83bf54a1ab8d9f5104be27740 Mon Sep 17 00:00:00 2001 From: JohannaPeanut <76495099+JohannaPeanut@users.noreply.github.com> Date: Wed, 12 Feb 2025 10:44:49 +0100 Subject: [PATCH 27/32] ContactsTable: add back link --- .../contacts/table/_components/ContactsTable.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/app/(loggedInProjects)/[projectSlug]/contacts/table/_components/ContactsTable.tsx b/src/app/(loggedInProjects)/[projectSlug]/contacts/table/_components/ContactsTable.tsx index 850f8d47..332f679b 100644 --- a/src/app/(loggedInProjects)/[projectSlug]/contacts/table/_components/ContactsTable.tsx +++ b/src/app/(loggedInProjects)/[projectSlug]/contacts/table/_components/ContactsTable.tsx @@ -1,7 +1,7 @@ "use client" import { ObjectDump } from "@/src/app/admin/_components/ObjectDump" import { improveErrorMessage } from "@/src/core/components/forms/improveErrorMessage" -import { pinkButtonStyles } from "@/src/core/components/links" +import { Link, pinkButtonStyles } from "@/src/core/components/links" import { ButtonWrapper } from "@/src/core/components/links/ButtonWrapper" import { useProjectSlug } from "@/src/core/routes/useProjectSlug" import { isProduction } from "@/src/core/utils" @@ -152,6 +152,9 @@ export const ContactsTable = () => { return ( <> + + Zurück zu externen Kontakten +
{errors.length > 0 ? (
    @@ -184,7 +187,6 @@ export const ContactsTable = () => {
- ({ From 08aa67ed116a05b0c821ffd07faca90144436bbb Mon Sep 17 00:00:00 2001 From: JohannaPeanut <76495099+JohannaPeanut@users.noreply.github.com> Date: Tue, 18 Feb 2025 08:56:38 +0100 Subject: [PATCH 28/32] SurveyResponse: add icon in atlas link --- .../feedback/EditableSurveyResponseMapAndStaticData.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/survey-responses/components/feedback/EditableSurveyResponseMapAndStaticData.tsx b/src/survey-responses/components/feedback/EditableSurveyResponseMapAndStaticData.tsx index 24125d29..39bf14ef 100644 --- a/src/survey-responses/components/feedback/EditableSurveyResponseMapAndStaticData.tsx +++ b/src/survey-responses/components/feedback/EditableSurveyResponseMapAndStaticData.tsx @@ -14,6 +14,7 @@ import getSurvey from "@/src/surveys/queries/getSurvey" import { Routes, useParam } from "@blitzjs/next" import { useMutation, useQuery } from "@blitzjs/rpc" import { EnvelopeIcon } from "@heroicons/react/20/solid" +import { ArrowsPointingOutIcon, ArrowUpRightIcon } from "@heroicons/react/24/outline" import { center, lineString, multiLineString } from "@turf/turf" import { clsx } from "clsx" import { LngLatBoundsLike } from "react-map-gl/maplibre" @@ -157,7 +158,8 @@ const EditableSurveyResponseMapAndStaticData = ({ return (
{atlasUrl && !showMap && ( - + + Im Radverkehrsatlas öffnen )} @@ -216,11 +218,14 @@ const EditableSurveyResponseMapAndStaticData = ({ responseDetails: response.id, selectedResponses: [response.id], })} + className="flex items-center gap-2" > + In großer Karte öffnen {atlasUrl && ( - + + Im Radverkehrsatlas öffnen )} From 9472f8de2b1006f355f6248fe4c362bba70e8b8b Mon Sep 17 00:00:00 2001 From: JohannaPeanut <76495099+JohannaPeanut@users.noreply.github.com> Date: Tue, 18 Feb 2025 09:08:05 +0100 Subject: [PATCH 29/32] Admin: update wording --- .../subsections/_components/SubsectionPlacemarkImport.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/admin/projects/[projectSlug]/subsections/_components/SubsectionPlacemarkImport.tsx b/src/app/admin/projects/[projectSlug]/subsections/_components/SubsectionPlacemarkImport.tsx index d8660684..a7d02e8d 100644 --- a/src/app/admin/projects/[projectSlug]/subsections/_components/SubsectionPlacemarkImport.tsx +++ b/src/app/admin/projects/[projectSlug]/subsections/_components/SubsectionPlacemarkImport.tsx @@ -128,7 +128,7 @@ export const SubsectionPlacemarkImport = ({ project }: Props) => { {project.exportEnabled ? ( - Geometrien in Placemark Play bearbeiten + Geometrien in Placemark Play öffnen ) : (
@@ -152,7 +152,7 @@ export const SubsectionPlacemarkImport = ({ project }: Props) => { "cursor-pointer", )} > - {selectedFile ? "Andere Datei auswählen" : "Datei auswählen"} + {selectedFile ? "Anderes Geojson hochladen" : "Neues Geojson hochladen"} Date: Tue, 18 Feb 2025 10:43:05 +0100 Subject: [PATCH 30/32] Admin: update wording --- .../_components/SubsectionPlacemarkImport.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/app/admin/projects/[projectSlug]/subsections/_components/SubsectionPlacemarkImport.tsx b/src/app/admin/projects/[projectSlug]/subsections/_components/SubsectionPlacemarkImport.tsx index a7d02e8d..5e6e59e4 100644 --- a/src/app/admin/projects/[projectSlug]/subsections/_components/SubsectionPlacemarkImport.tsx +++ b/src/app/admin/projects/[projectSlug]/subsections/_components/SubsectionPlacemarkImport.tsx @@ -89,7 +89,7 @@ export const SubsectionPlacemarkImport = ({ project }: Props) => { einschalten.
  • - Der Button unten `Geometrien in Placemark Play bearbeiten` öffnet alle Geometrien der + Der Button unten ` Geometrien in Placemark Play öffnen` öffnet alle Geometrien der Planungsabschnitte dieses Projekts in Placemark Play. Dort Geometrien bearbeiten. Alle neu angelegten Planungsabschnitte sind an ihrer Default-Geometrie zu erkennen. Diese bearbeiten oder löschen und neu anlegen - für den Re-import{" "} @@ -103,12 +103,13 @@ export const SubsectionPlacemarkImport = ({ project }: Props) => { in der Übersicht, kann der `slug` des Planungsabschnitts eingesehen und kopiert werden.
  • - Die bearbeiteten Geometrien in Placemark Play `File` - `Export` als GeoJSON speichern. + Die bearbeiteten Geometrien in Placemark Play über `File` - `Export` als GeoJSON + speichern.
  • - Die gespeicherte GeoJSON-Datei hier über `Datei auswählen` auswählen und auf `Geometrien - der Planungsabschnitte ersetzen` klicken, um die Geometrien der Planungsabschnitte zu - aktualisieren.{" "} + Die gespeicherte GeoJSON-Datei hier über `Neues Geojson hochladen` auswählen und auf + `Geometrien der Planungsabschnitte ersetzen` klicken, um die Geometrien der + Planungsabschnitte zu aktualisieren.{" "} Wichtig: Die Geometrien der Planungsabschnitte im TS werden durch die Geometrien in der hochgeladenen Datei ersetzt. From 196d6921ae5943f15402c558896b1e76f585bfd2 Mon Sep 17 00:00:00 2001 From: Tobias Date: Wed, 19 Feb 2025 10:21:11 +0100 Subject: [PATCH 31/32] Admin: Improve tutorial; fix mass-creation process --- package-lock.json | 15 +++ .../_components/SubsectionPlacemarkImport.tsx | 113 +++++++++++------- .../[projectSlug]/subsections/page.tsx | 8 +- .../mutations/createSubsections.ts | 7 +- 4 files changed, 96 insertions(+), 47 deletions(-) diff --git a/package-lock.json b/package-lock.json index b77dd20a..17dd39d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26539,6 +26539,21 @@ "optional": true } } + }, + "node_modules/react-email/node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.23", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.23.tgz", + "integrity": "sha512-zfHZOGguFCqAJ7zldTKg4tJHPJyJCOFhpoJcVxKL9BSUHScVDnMdDuOU1zPPGdOzr/GWxbhYTjyiEgLEpAoFPA==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } } } } diff --git a/src/app/admin/projects/[projectSlug]/subsections/_components/SubsectionPlacemarkImport.tsx b/src/app/admin/projects/[projectSlug]/subsections/_components/SubsectionPlacemarkImport.tsx index 5e6e59e4..7f60afb2 100644 --- a/src/app/admin/projects/[projectSlug]/subsections/_components/SubsectionPlacemarkImport.tsx +++ b/src/app/admin/projects/[projectSlug]/subsections/_components/SubsectionPlacemarkImport.tsx @@ -13,6 +13,7 @@ import clsx from "clsx" import { useRouter } from "next/navigation" import { useState } from "react" import { z } from "zod" +import { defaultGeometryForMultipleSubsectionForm } from "../multiple-new/_components/MultipleNewSubsectionsForm" type Props = { project: Awaited> } @@ -71,59 +72,84 @@ export const SubsectionPlacemarkImport = ({ project }: Props) => { return (
    -

    Prozess zum Aktualisieren der Geometrien der Planungsabschnitte eines Projekts:

    +

    Prozess zum Aktualisieren der Geometrien der Planungsabschnitte eines Projekts:

    +

    Vorbedingung

    1. - Nur für neue Planungsabschnitte:{" "} - - Hier - {" "} - mehrere Planungsabschnitte gleichzeitig anlegen. Die Geometrien der neuen - Planungsabschnitte werden mit einer Default-Geometrie angelegt. + Export-API für das Projekt aktivieren{" "} + + unter Projekt bearbeiten “✔️ Export-API aktiv” + + .
      + {project.exportEnabled ? ( + Aktuell ist der Export aktiv. + ) : ( + Aktuell ist der Export inaktiv! + )}
    2. +
    +

    Vorbereitung für neue Planungsabschnitte

    +
    1. - Export des Projekts{" "} - - hier - {" "} - einschalten. + + + Mehrere Planungsabschnitte gleichzeitig anlegen + + + .
    2. +
    + +

    + Die Abschnitte bekommen Platzhalter-Geometrien, die durch den Upload ersetzt werden. + Wichtig sind die slugs, die als Referenz im GeoJSON dienen. +
    + Planungsabschnitte mit Platzhalter-Geometrie: +

    + + {subsections + .filter((subsection) => { + return ( + String(subsection.geometry) === defaultGeometryForMultipleSubsectionForm.join(",") + ) + }) + .map((subsection) => { + return ( +
  • + + {subsection.slug} + +
  • + ) + })} + +

    Aktualisierung der Geometrien

    +
    1. - Der Button unten ` Geometrien in Placemark Play öffnen` öffnet alle Geometrien der - Planungsabschnitte dieses Projekts in Placemark Play. Dort Geometrien bearbeiten. Alle - neu angelegten Planungsabschnitte sind an ihrer Default-Geometrie zu erkennen. Diese - bearbeiten oder löschen und neu anlegen - für den Re-import{" "} - - wichtig: die property `subsectionSlug` der Geometrie in Placemark Play muss mit dem - `slug` des Planungsabschnitts im TS übereinstimmen. - {" "} - - Hier - {" "} - in der Übersicht, kann der `slug` des Planungsabschnitts eingesehen und kopiert werden. + Über den Button “Geometrien in Placemark Play öffnen” alle Geometrien der + Planungsabschnitte dieses Projekts in Placemark Play öffnen.
    2. - Die bearbeiteten Geometrien in Placemark Play über `File` - `Export` als GeoJSON - speichern. + Die Geometrien in Placemark Play bearbeiten. Alle neu angelegten Planungsabschnitte sind + an ihrer Default-Geometrie zu erkennen. Diese bearbeiten oder löschen und neu anlegen. +
      + Wichtig: Die Property subsectionSlug muss mit dem slug des + Planungsabschnittes übereinstimmen.
    3. - Die gespeicherte GeoJSON-Datei hier über `Neues Geojson hochladen` auswählen und auf - `Geometrien der Planungsabschnitte ersetzen` klicken, um die Geometrien der - Planungsabschnitte zu aktualisieren.{" "} - - Wichtig: Die Geometrien der Planungsabschnitte im TS werden durch die Geometrien in - der hochgeladenen Datei ersetzt. - {" "} - Dies passiert lediglich für die Planungsabshcnitte, für die in der Datei ein Feature mit - dem übereinstimmenden Slug gefunden wird. + Die bearbeiteten Geometrien in Placemark Play über “File” - + “Export” als GeoJSON speichern.
    4. - In der{" "} - - Übersicht der Planungsabschnitte - {" "} - sind die aktualisierten Planungsabschnitte danach blau hinterlegt. + Die GeoJSON-Datei unten über “Neues Geojson hochladen” auswählen und auf + “Geometrien der Planungsabschnitte ersetzen” klicken, um die bestehenden + Geometrien zu ersetzen. +
      + Hinweis: Es werden nur die Planungsabshcnitte aktualisiert, für die eine passende + Geometrie gefunden wurde. Wurden Geometrien gelöscht, müssen die zugehörigen + Planungsabschnitte manuell gelöscht werden.
    5. +
    6. Nach dem Ersetzen zeigt eine Seite an, welche Daten aktualisiert wurden.
    @@ -132,10 +158,9 @@ export const SubsectionPlacemarkImport = ({ project }: Props) => { Geometrien in Placemark Play öffnen ) : ( -
    - Um mehrere Geometrien gleichzeitig in Placemark Play zu bearbeiten, muss der Geometrie - Export des Projekts hier eingeschaltet werden. -
    + + Geometrien in Placemark Play öffnen – EXPORT API INAKTIV + )}
    {["FILE_SELECTED"].includes(fileState) && ( diff --git a/src/app/admin/projects/[projectSlug]/subsections/page.tsx b/src/app/admin/projects/[projectSlug]/subsections/page.tsx index 84726af1..bc9baaf5 100644 --- a/src/app/admin/projects/[projectSlug]/subsections/page.tsx +++ b/src/app/admin/projects/[projectSlug]/subsections/page.tsx @@ -17,6 +17,7 @@ export default async function AdminProjectSubsectionsPage({ }) { const project = await invoke(getProject, { projectSlug }) const { subsections } = await invoke(getSubsections, { projectSlug }) + return ( <> @@ -34,8 +35,11 @@ export default async function AdminProjectSubsectionsPage({ ) : (
    - Um mehrere Geometrien gleichzeitig in Placemark Play zu bearbeiten, muss der Geometrie - Export des Projekts hier eingeschaltet werden. + Um mehrere Geometrien gleichzeitig in Placemark Play zu bearbeiten, muss{" "} + + die Export-API des Projekts eingeschaltet werden + + .
    )} diff --git a/src/server/subsections/mutations/createSubsections.ts b/src/server/subsections/mutations/createSubsections.ts index ec39fed4..5fe3d2a6 100644 --- a/src/server/subsections/mutations/createSubsections.ts +++ b/src/server/subsections/mutations/createSubsections.ts @@ -12,7 +12,12 @@ import { SubsectionSchema } from "../schema" export const CreateSubsectionsSchema = ProjectSlugRequiredSchema.merge( z.object({ subsections: z.array( - SubsectionSchema.omit({ managerId: true, operatorId: true, description: true }), + SubsectionSchema.omit({ + managerId: true, + operatorId: true, + description: true, + subsubsectionStatusId: true, + }), ), }), ) From 9c55d84a019da2d765e925413e999caeda07de01 Mon Sep 17 00:00:00 2001 From: Tobias Date: Wed, 19 Feb 2025 11:32:05 +0100 Subject: [PATCH 32/32] Admin: Improve tutorial --- .../subsections/_components/SubsectionPlacemarkImport.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/admin/projects/[projectSlug]/subsections/_components/SubsectionPlacemarkImport.tsx b/src/app/admin/projects/[projectSlug]/subsections/_components/SubsectionPlacemarkImport.tsx index 7f60afb2..2f391295 100644 --- a/src/app/admin/projects/[projectSlug]/subsections/_components/SubsectionPlacemarkImport.tsx +++ b/src/app/admin/projects/[projectSlug]/subsections/_components/SubsectionPlacemarkImport.tsx @@ -135,6 +135,9 @@ export const SubsectionPlacemarkImport = ({ project }: Props) => {
    Wichtig: Die Property subsectionSlug muss mit dem slug des Planungsabschnittes übereinstimmen. +
    + Hinweis: Alle anderen Properties können ignoriert werden, da der Upload sie auch + ignorieren wird.
  • Die bearbeiteten Geometrien in Placemark Play über “File” - @@ -145,7 +148,7 @@ export const SubsectionPlacemarkImport = ({ project }: Props) => { “Geometrien der Planungsabschnitte ersetzen” klicken, um die bestehenden Geometrien zu ersetzen.
    - Hinweis: Es werden nur die Planungsabshcnitte aktualisiert, für die eine passende + Hinweis: Es werden nur die Planungsabschnitte aktualisiert, für die eine passende Geometrie gefunden wurde. Wurden Geometrien gelöscht, müssen die zugehörigen Planungsabschnitte manuell gelöscht werden.