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 557036e46..e5231650d 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/core/components/Modal.tsx b/src/core/components/Modal.tsx new file mode 100644 index 000000000..c9076110e --- /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 + handleClose: () => void + className?: string +} + +export const Modal: React.FC = ({ children, open, handleClose, className }) => { + return ( + + + +
+ + +
+
+ + +
{children}
+
+
+
+
+
+
+ ) +} 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/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-public/components/maps/SurveyMap.tsx b/src/survey-public/components/maps/SurveyMap.tsx index 483bb390d..cbd966bd4 100644 --- a/src/survey-public/components/maps/SurveyMap.tsx +++ b/src/survey-public/components/maps/SurveyMap.tsx @@ -110,7 +110,7 @@ export const SurveyMap: React.FC = ({ projectMap, className, set const { config } = projectMap return ( -
+
void } -type TPinContext = { +export type TPinContext = { pinPosition: { lng: number lat: number 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/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 (
+ + )}
>( + 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..a82e37f2a --- /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..5e31dfd2e --- /dev/null +++ b/src/survey-responses/components/feedback/ExternalSurveyResponseFormModal.tsx @@ -0,0 +1,123 @@ +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 ( + <> + + { + setOpen(false) + setPinPosition(null) + }} + > + + + + + + ) +} diff --git a/src/survey-responses/components/feedback/useFilteredResponses.ts b/src/survey-responses/components/feedback/useFilteredResponses.ts index 921b12a87..16674c6c7 100644 --- a/src/survey-responses/components/feedback/useFilteredResponses.ts +++ b/src/survey-responses/components/feedback/useFilteredResponses.ts @@ -10,10 +10,10 @@ export const useFilteredResponses = ( if (!operator || !statuses || !topics || !hasnotes) return responses - console.log({ operator }) - console.log({ statuses }) - console.log({ topics }) - console.log({ hasnotes }) + // console.log({ operator }) + // console.log({ statuses }) + // console.log({ topics }) + // console.log({ hasnotes }) const filtered = responses // Handle `operator` which is the `operatorId: number` as 'string' 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/mutations/deleteSurveyResponse.ts b/src/survey-responses/mutations/deleteSurveyResponse.ts new file mode 100644 index 000000000..55ab59a16 --- /dev/null +++ b/src/survey-responses/mutations/deleteSurveyResponse.ts @@ -0,0 +1,8 @@ +import { resolver } from "@blitzjs/rpc" +import db from "db" +import { DeleteSurveyResponseSchema } from "../../survey-responses/schema" + +export default resolver.pipe( + resolver.zod(DeleteSurveyResponseSchema), + async ({ id }) => await db.surveyResponse.deleteMany({ where: { id } }), +) diff --git a/src/survey-responses/schema.ts b/src/survey-responses/schema.ts index 921f7bebb..5878e8fed 100644 --- a/src/survey-responses/schema.ts +++ b/src/survey-responses/schema.ts @@ -1,11 +1,21 @@ -import { SurveyResponseStatusEnum } from "@prisma/client" -import { z } from "zod" +import { SurveyResponseSourceEnum, SurveyResponseStatusEnum } from "@prisma/client" +import { number, 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(), +}) + +export const DeleteSurveyResponseSchema = z.object({ id: z.number() })