From 2e878d96ea48374161e0445d10be5e0571060f46 Mon Sep 17 00:00:00 2001 From: JohannaPeanut <76495099+JohannaPeanut@users.noreply.github.com> Date: Wed, 10 Jan 2024 10:34:44 +0100 Subject: [PATCH 1/7] Core/components: add Modal --- src/core/components/Modal.tsx | 53 +++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/core/components/Modal.tsx diff --git a/src/core/components/Modal.tsx b/src/core/components/Modal.tsx new file mode 100644 index 000000000..196d7afb1 --- /dev/null +++ b/src/core/components/Modal.tsx @@ -0,0 +1,53 @@ +import { Dialog, Transition } from "@headlessui/react" +import clsx from "clsx" +import { Fragment } from "react" + +type Props = { + children?: React.ReactNode + open: boolean + setOpen: (open: boolean) => void + className?: string +} + +export const Modal: React.FC = ({ children, open, setOpen, className }) => { + return ( + + + +
+ + +
+
+ + +
{children}
+
+
+
+
+
+
+ ) +} From 6c5eb571b1e7d42f6f0def995f37d31427bb6609 Mon Sep 17 00:00:00 2001 From: JohannaPeanut <76495099+JohannaPeanut@users.noreply.github.com> Date: Wed, 10 Jan 2024 10:31:12 +0100 Subject: [PATCH 2/7] SurveyResponse: update schema, add field source --- .../migration.sql | 5 +++++ db/schema.prisma | 7 +++++++ src/survey-public/components/SurveyMainPage.tsx | 4 +++- .../components/feedback/EditableSurveyResponseForm.tsx | 2 +- src/survey-responses/mutations/createSurveyResponse.ts | 6 ++++-- src/survey-responses/schema.ts | 10 +++++++++- 6 files changed, 29 insertions(+), 5 deletions(-) create mode 100644 db/migrations/20240109075002_surveyresponse_add_field_source/migration.sql diff --git a/db/migrations/20240109075002_surveyresponse_add_field_source/migration.sql b/db/migrations/20240109075002_surveyresponse_add_field_source/migration.sql new file mode 100644 index 000000000..4f1974a99 --- /dev/null +++ b/db/migrations/20240109075002_surveyresponse_add_field_source/migration.sql @@ -0,0 +1,5 @@ +-- CreateEnum +CREATE TYPE "SurveyResponseSourceEnum" AS ENUM ('EMAIL', 'LETTER', 'FORM'); + +-- AlterTable +ALTER TABLE "SurveyResponse" ADD COLUMN "source" "SurveyResponseSourceEnum" NOT NULL DEFAULT 'FORM'; diff --git a/db/schema.prisma b/db/schema.prisma index 67f4350a8..bfec28d23 100644 --- a/db/schema.prisma +++ b/db/schema.prisma @@ -437,6 +437,7 @@ model SurveyResponse { data String status SurveyResponseStatusEnum? @default(PENDING) note String? + source SurveyResponseSourceEnum @default(FORM) // surveySession SurveySession @relation(fields: [surveySessionId], references: [id]) surveySessionId Int @@ -445,6 +446,12 @@ model SurveyResponse { surveyResponseTopics SurveyResponseTopicsOnSurveyResponses[] } +enum SurveyResponseSourceEnum { + EMAIL + LETTER + FORM +} + model SurveyResponseTopic { id Int @id @default(autoincrement()) title String diff --git a/src/survey-public/components/SurveyMainPage.tsx b/src/survey-public/components/SurveyMainPage.tsx index b85994832..78d0b5478 100644 --- a/src/survey-public/components/SurveyMainPage.tsx +++ b/src/survey-public/components/SurveyMainPage.tsx @@ -36,7 +36,7 @@ export const SurveyMainPage: React.FC = ({ responseConfig, surveyId, }) => { - const [stage, setStage] = useState<"SURVEY" | "MORE" | "FEEDBACK" | "EMAIL">("SURVEY") + const [stage, setStage] = useState<"SURVEY" | "MORE" | "FEEDBACK" | "EMAIL">("FEEDBACK") const [progress, setProgress] = useState(1) const [isSpinner, setIsSpinner] = useState(false) const [responses, setResponses] = useState([]) @@ -67,6 +67,7 @@ export const SurveyMainPage: React.FC = ({ surveySessionId: surveySessionId_, surveyPart: surveyDefinition.part, data: JSON.stringify(surveyResponses), + source: "FORM", }) })() @@ -91,6 +92,7 @@ export const SurveyMainPage: React.FC = ({ surveySessionId: surveySessionId_, surveyPart: feedbackDefinition.part, data: JSON.stringify(feedbackResponses), + source: "FORM", }) })() diff --git a/src/survey-responses/components/feedback/EditableSurveyResponseForm.tsx b/src/survey-responses/components/feedback/EditableSurveyResponseForm.tsx index 7e4cfbb97..4798b8828 100644 --- a/src/survey-responses/components/feedback/EditableSurveyResponseForm.tsx +++ b/src/survey-responses/components/feedback/EditableSurveyResponseForm.tsx @@ -82,6 +82,7 @@ export function EditableSurveyResponseForm>({ try { await updateSurveyResponseMutation({ id: response.id, + source: response.source, // We specify what we want to store explicity so that `data` and such is exclued status: values.status, note: values.note, @@ -116,7 +117,6 @@ export function EditableSurveyResponseForm>({ const newTopicTitle = values.newTopic?.trim() if (!newTopicTitle) return - console.log(router.query.topics) try { const createdOrFetched = await createSurveyResponseTopicMutation({ title: newTopicTitle, diff --git a/src/survey-responses/mutations/createSurveyResponse.ts b/src/survey-responses/mutations/createSurveyResponse.ts index 566105bca..267593084 100644 --- a/src/survey-responses/mutations/createSurveyResponse.ts +++ b/src/survey-responses/mutations/createSurveyResponse.ts @@ -1,14 +1,16 @@ import { resolver } from "@blitzjs/rpc" +import { SurveyResponseSourceEnum } from "@prisma/client" import db from "db" import { z } from "zod" -const CreateSurveyResponse = z.object({ +const CreateSurveyResponseSchema = z.object({ surveySessionId: z.number(), surveyPart: z.number(), data: z.string(), + source: z.nativeEnum(SurveyResponseSourceEnum), }) export default resolver.pipe( - resolver.zod(CreateSurveyResponse), + resolver.zod(CreateSurveyResponseSchema), async (input) => await db.surveyResponse.create({ data: input }), ) diff --git a/src/survey-responses/schema.ts b/src/survey-responses/schema.ts index 921f7bebb..cc77bac60 100644 --- a/src/survey-responses/schema.ts +++ b/src/survey-responses/schema.ts @@ -1,11 +1,19 @@ -import { SurveyResponseStatusEnum } from "@prisma/client" +import { SurveyResponseSourceEnum, SurveyResponseStatusEnum } from "@prisma/client" import { z } from "zod" export const SurveyResponseSchema = z.object({ data: z.string(), status: z.nativeEnum(SurveyResponseStatusEnum), + source: z.nativeEnum(SurveyResponseSourceEnum), surveySessionId: z.coerce.number(), surveyPart: z.coerce.number(), note: z.string().nullish(), operatorId: z.coerce.number().nullish(), }) + +export const ExternalSurveyResponseFormSchema = z.object({ + source: z.nativeEnum(SurveyResponseSourceEnum), + userText1: z.string().nonempty({ message: "Pflichtfeld." }), + categoryId: z.string(), + isLocation: z.string(), +}) From 62c579bd9299f7397667ea13b934abbb53c8b61b Mon Sep 17 00:00:00 2001 From: JohannaPeanut <76495099+JohannaPeanut@users.noreply.github.com> Date: Wed, 10 Jan 2024 10:34:50 +0100 Subject: [PATCH 3/7] SurveyResponse: add ExternalSurveyResponseForm in modal --- .../surveys/[surveyId]/responses/index.tsx | 3 + src/survey-public/context/contexts.ts | 2 +- .../feedback/ExternalSurveyResponseForm.tsx | 78 ++++++++++++ .../ExternalSurveyResponseFormMap.tsx | 42 +++++++ .../ExternalSurveyResponseFormModal.tsx | 116 ++++++++++++++++++ 5 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 src/survey-responses/components/feedback/ExternalSurveyResponseForm.tsx create mode 100644 src/survey-responses/components/feedback/ExternalSurveyResponseFormMap.tsx create mode 100644 src/survey-responses/components/feedback/ExternalSurveyResponseFormModal.tsx diff --git a/src/pages/[projectSlug]/surveys/[surveyId]/responses/index.tsx b/src/pages/[projectSlug]/surveys/[surveyId]/responses/index.tsx index 019787036..5951b0a7f 100644 --- a/src/pages/[projectSlug]/surveys/[surveyId]/responses/index.tsx +++ b/src/pages/[projectSlug]/surveys/[surveyId]/responses/index.tsx @@ -12,6 +12,7 @@ import getSubsections from "src/subsections/queries/getSubsections" import getSurveyResponseTopicsByProject from "src/survey-response-topics/queries/getSurveyResponseTopicsByProject" import { EditableSurveyResponseFilterForm } from "src/survey-responses/components/feedback/EditableSurveyResponseFilterForm" import EditableSurveyResponseListItem from "src/survey-responses/components/feedback/EditableSurveyResponseListItem" +import { ExternalSurveyResponseFormModal } from "src/survey-responses/components/feedback/ExternalSurveyResponseFormModal" import { useFilteredResponses } from "src/survey-responses/components/feedback/useFilteredResponses" import getFeedbackSurveyResponses from "src/survey-responses/queries/getFeedbackSurveyResponses" import { SurveyTabs } from "src/surveys/components/SurveyTabs" @@ -64,6 +65,8 @@ export const SurveyResponse = () => {

Beiträge aus Bürgerbeteiligung ({filteredResponses.length})

+ + diff --git a/src/survey-public/context/contexts.ts b/src/survey-public/context/contexts.ts index 1ce33f6b4..0dcfe62ab 100644 --- a/src/survey-public/context/contexts.ts +++ b/src/survey-public/context/contexts.ts @@ -5,7 +5,7 @@ type TProgressContext = { setProgress: (current: number) => void } -type TPinContext = { +export type TPinContext = { pinPosition: { lng: number lat: number diff --git a/src/survey-responses/components/feedback/ExternalSurveyResponseForm.tsx b/src/survey-responses/components/feedback/ExternalSurveyResponseForm.tsx new file mode 100644 index 000000000..224c9b31e --- /dev/null +++ b/src/survey-responses/components/feedback/ExternalSurveyResponseForm.tsx @@ -0,0 +1,78 @@ +import { MapProvider } from "react-map-gl" +import { + Form, + FormProps, + LabeledRadiobuttonGroup, + LabeledSelect, + LabeledTextareaField, +} from "src/core/components/forms" +import { H2 } from "src/core/components/text" +import { TMapProps, TResponse } from "src/survey-public/components/types" + +import { z } from "zod" +import { ExternalSurveyResponseFormMap } from "./ExternalSurveyResponseFormMap" + +export { FORM_ERROR } from "src/core/components/forms" + +export function ExternalSurveyResponseForm>( + props: FormProps & { + mapProps: TMapProps + categories: TResponse[] + isLocation: boolean + setIsLocation: any + }, +) { + const { mapProps, categories, isLocation, setIsLocation } = props + + return ( + {...props}> +

Neuen Hinweis erfassen

+

+ Hier können Sie die Beiträge erfassen, die abseits der Online-Beteiligung eingereicht + wurden. +

+ + + + + + + + + + + + { + return { value: String(category.id), label: category.text.de } + })} + /> + + ) +} diff --git a/src/survey-responses/components/feedback/ExternalSurveyResponseFormMap.tsx b/src/survey-responses/components/feedback/ExternalSurveyResponseFormMap.tsx new file mode 100644 index 000000000..8a852d0ee --- /dev/null +++ b/src/survey-responses/components/feedback/ExternalSurveyResponseFormMap.tsx @@ -0,0 +1,42 @@ +import { useState } from "react" +import { useFormContext } from "react-hook-form" +import { SurveyMap } from "src/survey-public/components/maps/SurveyMap" +import { TMapProps } from "src/survey-public/components/types" + +type Props = { mapProps: TMapProps; isLocation: boolean; setIsLocation: any } + +export const ExternalSurveyResponseFormMap: React.FC = ({ + mapProps, + isLocation, + setIsLocation, +}) => { + const { watch } = useFormContext() + // the isMapDirty state is useless atm - we just need it as a prop for surveyMap but we do not use it here (yet) + const [isMapDirty, setIsMapDirty] = useState(false) + + setIsLocation(watch("isLocation") === "true") + console.log("isMap", isLocation) + + if (!isLocation) return + + return ( +
+

Position wählen

+ +
+ ) +} diff --git a/src/survey-responses/components/feedback/ExternalSurveyResponseFormModal.tsx b/src/survey-responses/components/feedback/ExternalSurveyResponseFormModal.tsx new file mode 100644 index 000000000..02252eeda --- /dev/null +++ b/src/survey-responses/components/feedback/ExternalSurveyResponseFormModal.tsx @@ -0,0 +1,116 @@ +import { useParam } from "@blitzjs/next" +import { useMutation, useQuery } from "@blitzjs/rpc" +import { PlusIcon } from "@heroicons/react/20/solid" +import { SurveyResponseSourceEnum } from "@prisma/client" +import clsx from "clsx" +import { useState } from "react" +import { blueButtonStyles } from "src/core/components/links" +import { Modal } from "src/core/components/Modal" +import { + TMapProps, + TResponse, + TSingleOrMultiResponseProps, +} from "src/survey-public/components/types" +import { PinContext } from "src/survey-public/context/contexts" +import { + getFeedbackDefinitionBySurveySlug, + getResponseConfigBySurveySlug, +} from "src/survey-public/utils/getConfigBySurveySlug" +import createSurveyResponse from "src/survey-responses/mutations/createSurveyResponse" +import { ExternalSurveyResponseFormSchema } from "src/survey-responses/schema" +import createSurveySession from "src/survey-sessions/mutations/createSurveySession" +import getSurvey from "src/surveys/queries/getSurvey" +import { ExternalSurveyResponseForm, FORM_ERROR } from "./ExternalSurveyResponseForm" + +type Props = { refetch: any } + +export const ExternalSurveyResponseFormModal: React.FC = ({ refetch }) => { + const surveyId = useParam("surveyId", "string") + const [survey] = useQuery(getSurvey, { id: Number(surveyId) }) + + const [open, setOpen] = useState(false) + const [isLocation, setIsLocation] = useState(true) + const [pinPosition, setPinPosition] = useState(null) + + const [createSurveySessionMutation] = useMutation(createSurveySession) + const [createSurveyResponseMutation] = useMutation(createSurveyResponse) + + const { evaluationRefs } = getResponseConfigBySurveySlug(survey.slug) + const feedbackDefinition = getFeedbackDefinitionBySurveySlug(survey.slug) + + const feedbackQuestions = [] + + for (let page of feedbackDefinition.pages) { + feedbackQuestions.push(...page.questions) + } + + const categoryId = evaluationRefs["feedback-category"] + const locationId = evaluationRefs["feedback-location"] + const userText1Id = evaluationRefs["feedback-usertext-1"] + + const categorieQuestion = feedbackQuestions.find((q) => q.id === categoryId)! + .props as TSingleOrMultiResponseProps + const categories: TResponse[] = categorieQuestion!.responses + const mapProps = feedbackDefinition!.pages[0]!.questions.find( + (q) => q.id === evaluationRefs["feedback-location"], + )!.props as TMapProps + + type HandleSubmit = any // TODO + const handleSubmit = async (values: HandleSubmit) => { + try { + // in the future we will only have one data field for user text, but we have 2 data fields for user texts in the RS8 survey + // the following lines are a workaround to have a consistent datastructure among all survey responses of the same survey + const additionalUserTextId = evaluationRefs["feedback-usertext-2"] + const additionalUserTextObject = additionalUserTextId + ? { [additionalUserTextId]: null } + : null + + const data = { + [userText1Id!]: values.userText1 || null, + [categoryId!]: Number(values.categoryId), + [locationId!]: + pinPosition && isLocation ? { lng: pinPosition.lng, lat: pinPosition.lat } : null, + ...additionalUserTextObject, + } + console.log(data) + const surveySession = await createSurveySessionMutation({ surveyId: Number(surveyId) }) + const surveyResponse = await createSurveyResponseMutation({ + surveyPart: 2, + data: JSON.stringify(data), + surveySessionId: surveySession.id, + source: values.source as SurveyResponseSourceEnum, + }) + await refetch() + setOpen(false) + setPinPosition(null) + } catch (error: any) { + console.error(error) + return { [FORM_ERROR]: error } + } + } + return ( + <> + + + + + + + + ) +} From 5e4ea1056f9f5f4726667fc1a0ca9ac6f05c9837 Mon Sep 17 00:00:00 2001 From: JohannaPeanut <76495099+JohannaPeanut@users.noreply.github.com> Date: Wed, 10 Jan 2024 10:32:46 +0100 Subject: [PATCH 4/7] SurveyMap: update order className --- src/survey-public/components/maps/SurveyMap.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/survey-public/components/maps/SurveyMap.tsx b/src/survey-public/components/maps/SurveyMap.tsx index 483bb390d..26af79356 100644 --- a/src/survey-public/components/maps/SurveyMap.tsx +++ b/src/survey-public/components/maps/SurveyMap.tsx @@ -110,7 +110,8 @@ export const SurveyMap: React.FC = ({ projectMap, className, set const { config } = projectMap return ( -
+
+ {/*
*/} Date: Wed, 10 Jan 2024 15:25:35 +0100 Subject: [PATCH 5/7] SurveyResponse: fix styles, clean up --- src/survey-public/components/maps/SurveyMap.tsx | 1 - .../components/feedback/ExternalSurveyResponseFormMap.tsx | 2 +- .../feedback/ExternalSurveyResponseFormModal.tsx | 2 +- .../components/feedback/useFilteredResponses.ts | 8 ++++---- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/survey-public/components/maps/SurveyMap.tsx b/src/survey-public/components/maps/SurveyMap.tsx index 26af79356..cbd966bd4 100644 --- a/src/survey-public/components/maps/SurveyMap.tsx +++ b/src/survey-public/components/maps/SurveyMap.tsx @@ -111,7 +111,6 @@ export const SurveyMap: React.FC = ({ projectMap, className, set return (
- {/*
*/} = ({

Position wählen

= ({ refetch }) => Neuen Hinweis erfassen - + Date: Wed, 10 Jan 2024 15:26:10 +0100 Subject: [PATCH 6/7] SurveyResponse: mark external feedback, add delete button --- .../EditableSurveyResponseListItem.tsx | 37 ++++++++++++++++++- .../mutations/deleteSurveyResponse.ts | 8 ++++ src/survey-responses/schema.ts | 4 +- 3 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 src/survey-responses/mutations/deleteSurveyResponse.ts diff --git a/src/survey-responses/components/feedback/EditableSurveyResponseListItem.tsx b/src/survey-responses/components/feedback/EditableSurveyResponseListItem.tsx index 21fef3f9d..7d874896a 100644 --- a/src/survey-responses/components/feedback/EditableSurveyResponseListItem.tsx +++ b/src/survey-responses/components/feedback/EditableSurveyResponseListItem.tsx @@ -13,13 +13,16 @@ import { getSurveyResponseCategoryById } from "src/survey-responses/utils/getSur import { EditableSurveyResponseForm } from "./EditableSurveyResponseForm" import EditableSurveyResponseUserText from "./EditableSurveyResponseUserText" +import { useMutation, useQuery } from "@blitzjs/rpc" +import { EnvelopeIcon } from "@heroicons/react/24/outline" +import { linkStyles } from "src/core/components/links" +import { TMapProps } from "src/survey-public/components/types" import { getFeedbackDefinitionBySurveySlug, getResponseConfigBySurveySlug, } from "src/survey-public/utils/getConfigBySurveySlug" -import { TMapProps } from "src/survey-public/components/types" +import deleteSurveyResponse from "src/survey-responses/mutations/deleteSurveyResponse" import getSurvey from "src/surveys/queries/getSurvey" -import { useQuery } from "@blitzjs/rpc" export type EditableSurveyResponseListItemProps = { response: Prettify>[number]> @@ -52,6 +55,7 @@ const EditableSurveyResponseListItem: React.FC { + switch (s) { + case "LETTER": + return "Brief" + default: + return "Email" + } + } + + const handleDelete = async () => { + if ( + response.source !== "FORM" && + window.confirm(`Den Eintrag mit ID ${response.id} unwiderruflich löschen?`) + ) { + await deleteCalendarEntryMutation({ id: response.id }) + await refetchResponsesAndTopics() + } + } + return (
+ + )}
await db.surveyResponse.deleteMany({ where: { id } }), +) diff --git a/src/survey-responses/schema.ts b/src/survey-responses/schema.ts index cc77bac60..5878e8fed 100644 --- a/src/survey-responses/schema.ts +++ b/src/survey-responses/schema.ts @@ -1,5 +1,5 @@ import { SurveyResponseSourceEnum, SurveyResponseStatusEnum } from "@prisma/client" -import { z } from "zod" +import { number, z } from "zod" export const SurveyResponseSchema = z.object({ data: z.string(), @@ -17,3 +17,5 @@ export const ExternalSurveyResponseFormSchema = z.object({ categoryId: z.string(), isLocation: z.string(), }) + +export const DeleteSurveyResponseSchema = z.object({ id: z.number() }) From 0925dfbd6b1c961041b787edfbfe08f43bd6fc2f Mon Sep 17 00:00:00 2001 From: JohannaPeanut <76495099+JohannaPeanut@users.noreply.github.com> Date: Wed, 10 Jan 2024 15:36:52 +0100 Subject: [PATCH 7/7] SurveyResponse: reset pin position when Modal closes --- src/core/components/Modal.tsx | 6 +++--- .../feedback/ExternalSurveyResponseFormModal.tsx | 9 ++++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/core/components/Modal.tsx b/src/core/components/Modal.tsx index 196d7afb1..c9076110e 100644 --- a/src/core/components/Modal.tsx +++ b/src/core/components/Modal.tsx @@ -5,14 +5,14 @@ import { Fragment } from "react" type Props = { children?: React.ReactNode open: boolean - setOpen: (open: boolean) => void + handleClose: () => void className?: string } -export const Modal: React.FC = ({ children, open, setOpen, className }) => { +export const Modal: React.FC = ({ children, open, handleClose, className }) => { return ( - + = ({ refetch }) => Neuen Hinweis erfassen - + { + setOpen(false) + setPinPosition(null) + }} + >