Skip to content

Commit

Permalink
Form for external survey responses (#79)
Browse files Browse the repository at this point in the history
  • Loading branch information
JohannaPeanut authored Jan 10, 2024
2 parents 6e1c7cf + 0925dfb commit 5d6644f
Show file tree
Hide file tree
Showing 16 changed files with 380 additions and 14 deletions.
Original file line number Diff line number Diff line change
@@ -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';
7 changes: 7 additions & 0 deletions db/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -445,6 +446,12 @@ model SurveyResponse {
surveyResponseTopics SurveyResponseTopicsOnSurveyResponses[]
}

enum SurveyResponseSourceEnum {
EMAIL
LETTER
FORM
}

model SurveyResponseTopic {
id Int @id @default(autoincrement())
title String
Expand Down
53 changes: 53 additions & 0 deletions src/core/components/Modal.tsx
Original file line number Diff line number Diff line change
@@ -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<Props> = ({ children, open, handleClose, className }) => {
return (
<Transition.Root show={open} as={Fragment}>
<Dialog as="div" className="relative z-20" onClose={handleClose}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity" />
</Transition.Child>

<div className="fixed inset-0 z-10 w-screen overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enterTo="opacity-100 translate-y-0 sm:scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<Dialog.Panel
className={clsx(
"relative transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-sm sm:p-6",
className,
)}
>
<div className="mt-5 sm:mt-6">{children}</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition.Root>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -64,6 +65,8 @@ export const SurveyResponse = () => {
<div className="space-y-4 mt-12">
<H2>Beiträge aus Bürgerbeteiligung ({filteredResponses.length})</H2>

<ExternalSurveyResponseFormModal refetch={refetchResponses} />

<EditableSurveyResponseFilterForm operators={operators} topics={topics} />

<ZeroCase visible={filteredResponses.length} name={"Beiträge"} />
Expand Down
4 changes: 3 additions & 1 deletion src/survey-public/components/SurveyMainPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const SurveyMainPage: React.FC<Props> = ({
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<any[]>([])
Expand Down Expand Up @@ -67,6 +67,7 @@ export const SurveyMainPage: React.FC<Props> = ({
surveySessionId: surveySessionId_,
surveyPart: surveyDefinition.part,
data: JSON.stringify(surveyResponses),
source: "FORM",
})
})()

Expand All @@ -91,6 +92,7 @@ export const SurveyMainPage: React.FC<Props> = ({
surveySessionId: surveySessionId_,
surveyPart: feedbackDefinition.part,
data: JSON.stringify(feedbackResponses),
source: "FORM",
})
})()

Expand Down
2 changes: 1 addition & 1 deletion src/survey-public/components/maps/SurveyMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export const SurveyMap: React.FC<SurveyMapProps> = ({ projectMap, className, set
const { config } = projectMap

return (
<div className={clsx(className, "h-[500px]")}>
<div className={clsx("h-[500px]", className)}>
<Map
id="mainMap"
initialViewState={{
Expand Down
2 changes: 1 addition & 1 deletion src/survey-public/context/contexts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ type TProgressContext = {
setProgress: (current: number) => void
}

type TPinContext = {
export type TPinContext = {
pinPosition: {
lng: number
lat: number
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export function EditableSurveyResponseForm<S extends z.ZodType<any, any>>({
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,
Expand Down Expand Up @@ -116,7 +117,6 @@ export function EditableSurveyResponseForm<S extends z.ZodType<any, any>>({
const newTopicTitle = values.newTopic?.trim()
if (!newTopicTitle) return

console.log(router.query.topics)
try {
const createdOrFetched = await createSurveyResponseTopicMutation({
title: newTopicTitle,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<Awaited<ReturnType<typeof getFeedbackSurveyResponses>>[number]>
Expand Down Expand Up @@ -52,6 +55,7 @@ const EditableSurveyResponseListItem: React.FC<EditableSurveyResponseListItemPro
const open = parseInt(String(params.responseDetails)) === response.id
const surveyId = useParam("surveyId", "string")
const [survey] = useQuery(getSurvey, { id: Number(surveyId) })
const [deleteCalendarEntryMutation] = useMutation(deleteSurveyResponse)

const operatorSlugWitFallback = response.operator?.slug || "k.A."

Expand Down Expand Up @@ -91,6 +95,25 @@ const EditableSurveyResponseListItem: React.FC<EditableSurveyResponseListItemPro
feedbackQuestion!,
)

const getTranslatedSource = (s: string) => {
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 (
<article data-open={open}>
<button
Expand Down Expand Up @@ -124,6 +147,16 @@ const EditableSurveyResponseListItem: React.FC<EditableSurveyResponseListItemPro

{open && (
<div className={clsx("overflow-clip p-6", open ? "border-b border-gray-300" : "")}>
{response.source !== "FORM" && (
<span className="flex flex-row gap-2 items-center">
<EnvelopeIcon className="h-4 w-4" />
<span>per {getTranslatedSource(response.source)} eingegangen </span>
<span>| </span>
<button onClick={handleDelete} className={clsx(linkStyles, "my-0")}>
Eintrag löschen
</button>
</span>
)}
<div className="flex gap-12 mb-10 flex-col md:flex-row justify-between">
<EditableSurveyResponseUserText
surveyId={surveyId!}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<S extends z.ZodType<any, any>>(
props: FormProps<S> & {
mapProps: TMapProps
categories: TResponse[]
isLocation: boolean
setIsLocation: any
},
) {
const { mapProps, categories, isLocation, setIsLocation } = props

return (
<Form<S> {...props}>
<H2>Neuen Hinweis erfassen</H2>
<p>
Hier können Sie die Beiträge erfassen, die abseits der Online-Beteiligung eingereicht
wurden.
</p>

<LabeledRadiobuttonGroup
label="Bezieht sich das Feedback auf eine konkrete Stelle entlang der Route?"
scope="isLocation"
items={[
{ value: "true", label: "Ja" },
{ value: "false", label: "Nein" },
]}
/>

<MapProvider>
<ExternalSurveyResponseFormMap
isLocation={isLocation}
setIsLocation={setIsLocation}
mapProps={mapProps}
/>
</MapProvider>

<LabeledTextareaField
className="h-28"
label="Hinweistext"
placeholder="Hinweis hier einfügen..."
name="userText1"
/>

<LabeledSelect
name="source"
label="Eingereicht"
options={[
["EMAIL", "per E-Mail"],
["LETTER", "per Brief"],
]}
/>

<LabeledRadiobuttonGroup
label="Kategorie"
scope="categoryId"
classNameItemWrapper="sm:columns-2"
items={categories.map((category) => {
return { value: String(category.id), label: category.text.de }
})}
/>
</Form>
)
}
Original file line number Diff line number Diff line change
@@ -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<Props> = ({
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 (
<div>
<p className="mb-4 block text-sm font-medium text-gray-700">Position wählen</p>
<SurveyMap
className="!h-64"
projectMap={{
maptilerStyleUrl: mapProps.maptilerStyleUrl,
initialMarker: {
lng: mapProps.marker!.lng,
lat: mapProps.marker!.lat,
},
config: {
bounds: mapProps.config!.bounds,
pinColor: mapProps.config!.pinColor,
},
}}
setIsMapDirty={setIsMapDirty}
/>
</div>
)
}
Loading

0 comments on commit 5d6644f

Please sign in to comment.