diff --git a/.github/workflows/azure.yml b/.github/workflows/azure.yml index fe9e54d98..e5b624990 100644 --- a/.github/workflows/azure.yml +++ b/.github/workflows/azure.yml @@ -19,7 +19,7 @@ jobs: creds: ${{ secrets.AZURE_CREDENTIALS }} - name: "Checkout GitHub Action" - uses: actions/checkout@master + uses: actions/checkout@v4 - name: Extract branch name shell: bash diff --git a/.github/workflows/cypress.yml b/.github/workflows/cypress.yml index 67a115230..3cd3d1e9e 100644 --- a/.github/workflows/cypress.yml +++ b/.github/workflows/cypress.yml @@ -20,10 +20,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: '22' cache: 'yarn' diff --git a/.github/workflows/github.yml b/.github/workflows/github.yml index a4e05fa32..412a067f5 100644 --- a/.github/workflows/github.yml +++ b/.github/workflows/github.yml @@ -18,7 +18,7 @@ jobs: contents: read steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Build image run: docker build . --file Dockerfile --tag $IMAGE_NAME --label "runnumber=${GITHUB_RUN_ID}" diff --git a/components/blocks/block-list/index.tsx b/components/blocks/block-list/index.tsx index 0dd99139c..31878312b 100644 --- a/components/blocks/block-list/index.tsx +++ b/components/blocks/block-list/index.tsx @@ -22,6 +22,7 @@ import { } from "@/graphql/__generated__/operations"; import { CtaCardBlock } from "../cta-card-block"; +import { FortroendemodellenFrom } from "../fortroendemodellen-v2"; interface blockListProps { blocks: | ContainerDataFragment["blocks"] @@ -31,6 +32,7 @@ interface blockListProps { | StartPageDataFragment["blocks"]; className?: string; landingPage?: boolean; + formPage?: boolean; } /** @@ -72,6 +74,7 @@ export const BlockList: FC = ({ blocks, className, landingPage, + formPage, }) => { const { t } = useTranslation(); @@ -116,6 +119,7 @@ export const BlockList: FC = ({ {...block} key={getUniqueKey(block, index)} landingPage={landingPage} + formPage={formPage} /> ); case "dataportal_Digg_ModuleList": { @@ -165,6 +169,8 @@ export const BlockList: FC = ({ ); case "dataportal_Digg_CTACardBlock": return ; + case "dataportal_Digg_FoertroendemodellenBlock": + return ; default: { const unknownBlock = block as { __typename: string; id: string }; return ( diff --git a/components/blocks/fortroendemodellen-v2/index.tsx b/components/blocks/fortroendemodellen-v2/index.tsx new file mode 100644 index 000000000..c0d2ad7c7 --- /dev/null +++ b/components/blocks/fortroendemodellen-v2/index.tsx @@ -0,0 +1,494 @@ +import { usePathname } from "next/navigation"; +import { useRouter } from "next/router"; +import useTranslation from "next-translate/useTranslation"; +import { ChangeEvent, useEffect, useRef, useState } from "react"; + +import { Button } from "@/components/button"; +import { FormEnding } from "@/components/form/form-ending"; +import { RenderForm } from "@/components/form/render-form"; +import { Container } from "@/components/layout/container"; +import { FormBottomNav } from "@/components/navigation/form-bottom-nav"; +import { FormNav } from "@/components/navigation/form-nav"; +import { Heading } from "@/components/typography/heading"; +import { + FormDataFragment, + ContainerDataFragment, +} from "@/graphql/__generated__/operations"; +import { FormTypes } from "@/types/form"; +import { + GetLocalstorageData, + handleScroll, + fetchFortroendemodellenForm, +} from "@/utilities/form-utils"; + +import { BlockList } from "../block-list"; + +// import { FormGeneratePDF } from "@/components/form/form-generate-pdf"; + +export interface FormData { + id: string; + elements: FormDataFragment["elements"]; + blocks?: ContainerDataFragment["blocks"]; + resultPageInfo?: string; +} + +export const FortroendemodellenFrom = () => { + const router = useRouter(); + const { t } = useTranslation(); + const pathname = usePathname(); + const [formData, setFormData] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [page, setPage] = useState(1); + const scrollRef = useRef(null); + const [formDataArray, setFormDataArray] = useState>>( + [], + ); + const [formSteps, setFormSteps] = useState([]); + const [showFirstPage, setShowFirstPage] = useState(true); + const [formIntroText, setFormIntroText] = useState<{ + title: string; + text: string; + }>({ title: "", text: "" }); + let questionNumber = 1; + + const informationSection = [ + { + ID: 1, + info: null, + number: 1, + required: false, + title: t("pages|form$organisation-number"), + value: "", + __typename: "organisationNumber", + }, + { + ID: 2, + info: null, + number: 2, + required: false, + title: t("pages|form$organisation-name"), + value: "", + __typename: "dataportal_Digg_FormText", + }, + { + ID: 3, + info: null, + number: 3, + required: false, + title: t("pages|form$ai-system-name"), + value: "", + __typename: "dataportal_Digg_FormText", + }, + ]; + const getFortroendemodellenForm = async () => { + try { + setLoading(true); + setError(null); + const data = await fetchFortroendemodellenForm(router.locale || "sv"); + data.elements = [...informationSection, ...data.elements]; + // Add IDs to elements + if (data.elements) { + (data.elements as FormTypes[]).forEach((element, index) => { + element.ID = index; + }); + } + + setFormData(data); + } catch (err) { + console.error("Error fetching form data:", err); + setError(err instanceof Error ? err.message : "An error occurred"); + } finally { + setLoading(false); + } + }; + + // Fetch form data when router is ready + useEffect(() => { + if (router.isReady) { + getFortroendemodellenForm(); + } + }, [router.isReady, router.locale]); + + // Process form elements when formData changes + useEffect(() => { + if (!formData?.elements || formData.elements.length === 0) { + return; + } + + const elements = formData.elements; + SetupPages(elements as FormTypes[]); + GetLocalstorageData(setFormDataArray, elements, pathname); + }, [formData, pathname]); + + const SetupPages = (data: FormTypes[]) => { + if (data == null) { + return; + } + let currentPage: Array = []; + const pageArray: Array> = []; + setFormSteps([]); + + initializeFields(data); + + let checkTopHeading = true; + data.forEach((item, i) => { + if (i === 0 && item.__typename === "dataportal_Digg_FormDescription") { + return; + } + if (item.__typename === "dataportal_Digg_FormPageBreak") { + setFormSteps((prev) => [...prev, item.title]); + if (currentPage.length > 1) { + pageArray.push(currentPage); + currentPage = []; + checkTopHeading = true; + } + currentPage.push(item); + } else { + if ( + checkTopHeading && + item.__typename === "dataportal_Digg_FormDescription" + ) { + item.TopHeading = true; + } + checkTopHeading = false; + currentPage.push(item); + } + }); + + if (currentPage.length > 0) { + pageArray.push(currentPage); + } + setFormDataArray(pageArray); + }; + + const initializeFields = (data: FormTypes[]) => { + data.forEach((item) => { + setupItem(item); + if (item.__typename === "dataportal_Digg_FormRadio") { + // Don't set any default selection + item.selected = null; + item.choices.forEach((choice, i) => { + if (choice.popup === null) { + choice.popup = ""; + choice.exploratory = false; + } + choice.ID = parseInt(`${item.ID}${i}`); + }); + } + if (item.__typename === "dataportal_Digg_FormDescription") { + item.TopHeading = false; + } + }); + + if (data[0].__typename === "dataportal_Digg_FormDescription") { + setShowFirstPage(true); + setFormIntroText({ + title: data[0].title, + text: data[0].text.markdown || "", + }); + } else { + setShowFirstPage(false); + } + }; + + const setupItem = (item: FormTypes) => { + if ( + item.__typename === "dataportal_Digg_FormText" || + item.__typename === "dataportal_Digg_FormTextArea" || + item.__typename === "dataportal_Digg_FormRadio" || + item.__typename === "dataportal_Digg_FormDropdown" || + item.__typename === "dataportal_Digg_FormCheckbox" || + item.__typename === "organisationNumber" + ) { + if (item.__typename === "dataportal_Digg_FormDropdown") { + item.selected = null; + } + item.value = ""; + item.number = questionNumber; + questionNumber++; + } + }; + + const countQuestionsPerSection = () => { + const questionsPerSection: { + title: string; + count: number; + answered: number; + }[] = []; + let currentSection = { title: "Start", count: 0, answered: 0 }; + + formDataArray.forEach((page) => { + page.forEach((item) => { + if (item.__typename === "dataportal_Digg_FormPageBreak") { + // Save the previous section + if (currentSection.count > 0) { + questionsPerSection.push(currentSection); + } + // Start a new section + currentSection = { title: item.title, count: 0, answered: 0 }; + } else if ( + item.__typename === "dataportal_Digg_FormText" || + item.__typename === "dataportal_Digg_FormTextArea" || + item.__typename === "dataportal_Digg_FormRadio" || + item.__typename === "dataportal_Digg_FormDropdown" || + item.__typename === "dataportal_Digg_FormCheckbox" + ) { + currentSection.count++; + // Check if the question is answered + if ( + (item.__typename === "dataportal_Digg_FormText" || + item.__typename === "dataportal_Digg_FormTextArea") && + item.value !== "" + ) { + currentSection.answered++; + } else if ( + (item.__typename === "dataportal_Digg_FormRadio" || + item.__typename === "dataportal_Digg_FormDropdown") && + item.selected !== null + ) { + currentSection.answered++; + } else if ( + item.__typename === "dataportal_Digg_FormCheckbox" && + Array.isArray(item.selected) && + item.selected.length > 0 + ) { + currentSection.answered++; + } + } + }); + }); + + // Add the last section if it has questions + if (currentSection.count > 0) { + questionsPerSection.push(currentSection); + } + + return questionsPerSection; + }; + + useEffect(() => { + const pageLastVisit = localStorage.getItem(`${pathname}Page`); + if (!showFirstPage) { + setPage(pageLastVisit ? parseInt(pageLastVisit) : 1); + } else { + setPage(pageLastVisit ? parseInt(pageLastVisit) : 0); + } + }, [showFirstPage, pathname]); + + useEffect(() => { + if (page === 0) return; + localStorage.setItem(`${pathname}Page`, page.toString()); + }, [page, pathname]); + + const UpdateFormDataArray = ( + e: ChangeEvent, + fieldToUpdate: FormTypes, + pageIndex: number, + imgData: { fileName: string; base64: string } | null = null, + ) => { + pageIndex = pageIndex - 1; + + if (fieldToUpdate.__typename === "dataportal_Digg_FormCheckbox") { + setFormDataArray((prev) => { + const itemIndex = prev[pageIndex].findIndex( + (item) => item.ID === fieldToUpdate.ID, + ); + const foundObj = prev[pageIndex][itemIndex]; + if (foundObj && "choices" in foundObj) { + // Initialize selected as array if it doesn't exist or is not an array + if (!foundObj.selected || !Array.isArray(foundObj.selected)) { + foundObj.selected = []; + } + // Find the choice in the choices array that matches the value + const choice = foundObj.choices.find( + (c) => c.label === (e.target as HTMLInputElement).value, + ); + if (choice) { + // Add or remove the choice from the selected array based on checkbox state + const choiceIndex = foundObj.selected.findIndex( + (c) => c.label === choice.label, + ); + if ((e.target as HTMLInputElement).checked) { + if (choiceIndex === -1) { + foundObj.selected.push(choice); + } + } else { + if (choiceIndex !== -1) { + foundObj.selected.splice(choiceIndex, 1); + } + } + } + } + return [...prev]; + }); + } else if (fieldToUpdate.__typename === "dataportal_Digg_FormChoice") { + setFormDataArray((prev) => { + const itemIndex = prev[pageIndex].findIndex( + (item) => + "choices" in item && + item.choices.some((choice) => choice.ID === fieldToUpdate.ID), + ); + const foundObj = prev[pageIndex][itemIndex]; + if (foundObj && "choices" in foundObj) { + foundObj.selected = fieldToUpdate; + } + return [...prev]; + }); + } else if (fieldToUpdate.__typename === "dataportal_Digg_FormDropdown") { + setFormDataArray((prev) => { + const itemIndex = prev[pageIndex].findIndex( + (item) => item.ID === fieldToUpdate.ID, + ); + const foundObj = prev[pageIndex][itemIndex]; + if (foundObj && "items" in foundObj) { + const selectedItem = foundObj.items.find( + (item) => item.value === e.target.value, + ); + if (selectedItem) { + foundObj.selected = selectedItem; + foundObj.value = selectedItem.value ?? ""; + } + } + return [...prev]; + }); + } else { + setFormDataArray((prev) => { + const itemIndex = prev[pageIndex].findIndex( + (item) => item.ID === fieldToUpdate.ID, + ); + const foundObj = prev[pageIndex][itemIndex]; + if ("value" in foundObj) { + if (imgData) { + checkForImage(foundObj, imgData); + } + foundObj.value = e.target.value; + prev[pageIndex][itemIndex] = foundObj; + } + return [...prev]; + }); + } + setTimeout(() => { + localStorage.setItem(`${pathname}Data`, JSON.stringify(formDataArray)); + }, 100); + }; + + function checkForImage( + foundObj: FormTypes, + isImg: { fileName: string; base64: string }, + ) { + if ( + foundObj.__typename === "dataportal_Digg_FormTextArea" && + isImg.base64.includes("data:image") + ) { + if (foundObj.images === undefined) { + foundObj.images = {}; + } + foundObj.images[isImg.fileName] = isImg.base64; + } + } + + if (loading) { + return ( + +
Loading...
+
+ ); + } + + if (error) { + return ( + +
Error: {error}
+
+ ); + } + + return ( + <> + {formDataArray[0] && ( +
+ {page !== 0 && formSteps.length > 0 && ( + + )} + + <> + {page === 0 && ( + <> + {formIntroText.title.length > 0 && ( + <> + + {formIntroText.title} + +

{formIntroText.text}

+ + )} +
+ )} + {page === formDataArray.length + 1 && ( +
+ {formData?.blocks && } +
+ )} + + ); +}; diff --git a/components/blocks/related-content-block/index.tsx b/components/blocks/related-content-block/index.tsx index 73fea12ed..4fac14fb1 100644 --- a/components/blocks/related-content-block/index.tsx +++ b/components/blocks/related-content-block/index.tsx @@ -1,3 +1,4 @@ +import { cx } from "class-variance-authority"; import useTranslation from "next-translate/useTranslation"; import { FC } from "react"; @@ -8,6 +9,7 @@ import { RelatedContentFragment } from "@/graphql/__generated__/operations"; interface RelatedContentProps extends RelatedContentFragment { landingPage?: boolean; + formPage?: boolean; } export const RelatedContentBlock: FC = ({ @@ -15,6 +17,7 @@ export const RelatedContentBlock: FC = ({ heading, showMoreLink, landingPage, + formPage, }) => { const { t } = useTranslation("pages"); @@ -42,9 +45,12 @@ export const RelatedContentBlock: FC = ({ )}
    {links.map((link: PromoProps, idx: number) => { return ( diff --git a/components/form/form-ending/index.tsx b/components/form/form-ending/index.tsx new file mode 100644 index 000000000..8f67216d3 --- /dev/null +++ b/components/form/form-ending/index.tsx @@ -0,0 +1,165 @@ +import { usePathname } from "next/navigation"; +import useTranslation from "next-translate/useTranslation"; +import { FC, useEffect } from "react"; + +import { FormData } from "@/components/blocks/fortroendemodellen-v2"; +import { Heading } from "@/components/typography/heading"; +import { Preamble } from "@/components/typography/preamble"; +import { FormTypes } from "@/types/form"; + +type Props = { + formDataArray: FormTypes[][]; + formData: FormData; + countQuestionsPerSection: { + title: string; + count: number; + answered: number; + }[]; +}; + +interface QuestionWithRisk { + title: string; + risk: string; + answer: string; + number: number; +} + +export const FormEnding: FC = ({ + formDataArray, + formData, + countQuestionsPerSection, +}) => { + const { t } = useTranslation(); + const pathname = usePathname(); + useEffect(() => { + // Find the name and organization number fields in the form data + const nameField = formDataArray[0].find( + (item) => + item.__typename === "dataportal_Digg_FormText" && + item.title.toLowerCase() === "vad heter ai-systemet?", + ); + + // Track the event with Matomo + if (window._paq) { + const name = nameField && "value" in nameField ? nameField.value : ""; + const organisationNumber = localStorage.getItem(`${pathname}OrgNumber`); + + window._paq.push([ + "trackEvent", + "Förtroendemodellen - formulär utfört", + `Organisationsnummer: ${organisationNumber || "ej ifyllt"}`, + `AI-systemets namn: ${name || "ej ifyllt"}`, + ]); + } + }, [formDataArray]); + + const questionStats = countQuestionsPerSection.reduce( + (stats, section) => ({ + total: stats.total + section.count, + answered: stats.answered + section.answered, + }), + { total: 0, answered: 0 }, + ); + + const getQuestionsWithRisks = (): QuestionWithRisk[] => { + const questionsWithRisks: QuestionWithRisk[] = []; + + formDataArray.forEach((page) => { + page.forEach((item) => { + if ( + item.__typename === "dataportal_Digg_FormRadio" || + item.__typename === "dataportal_Digg_FormCheckbox" || + item.__typename === "dataportal_Digg_FormDropdown" + ) { + // Handle Radio and Dropdown (single selection) + if ( + (item.__typename === "dataportal_Digg_FormRadio" || + item.__typename === "dataportal_Digg_FormDropdown") && + item.selected && + typeof item.selected === "object" && + "popup" in item.selected && + item.selected.popup + ) { + questionsWithRisks.push({ + title: item.title, + risk: item.selected.popup, + answer: item.selected.label || item.selected.value || "", + number: item.number, + }); + } + // Handle Checkbox (multiple selections) + else if ( + item.__typename === "dataportal_Digg_FormCheckbox" && + Array.isArray(item.selected) + ) { + item.selected.forEach((choice) => { + if ( + choice && + typeof choice === "object" && + "popup" in choice && + choice.popup + ) { + questionsWithRisks.push({ + title: item.title, + risk: choice.popup, + answer: choice.label, + number: item.number, + }); + } + }); + } + } + }); + }); + + return questionsWithRisks; + }; + + const questionsWithRisks = getQuestionsWithRisks(); + + return ( +
    + {t("pages|form$summary")} + + {formData.resultPageInfo} + + {questionStats.total !== questionStats.answered && ( + + {t("pages|form$not-all-questions-answered")} ({" "} + {questionStats.total - questionStats.answered} + {questionStats.total - questionStats.answered === 1 + ? t("pages|form$question-remaining") + : t("pages|form$questions-remaining")}{" "} + ) + + )} +
    + {questionsWithRisks.length > 0 ? ( +
    + + {t("pages|form$risks-title")} + +
    + {questionsWithRisks.map((item, index) => ( +
    + + {t("pages|form$question")} {item.number} + + {item.title} + + {t("pages|form$answer")} {item.answer} + + {item.risk} +
    + ))} +
    +
    + ) : ( + + {t("pages|form$no-risk")} + + )} +
    +
    + ); +}; diff --git a/components/form/label/index.tsx b/components/form/label/index.tsx index 4c617cbab..4dce226ae 100644 --- a/components/form/label/index.tsx +++ b/components/form/label/index.tsx @@ -5,7 +5,7 @@ export const Label: FC< > = ({ children, className, ...props }) => (