diff --git a/.changeset/shy-hotels-tie.md b/.changeset/shy-hotels-tie.md new file mode 100644 index 00000000..c7af2413 --- /dev/null +++ b/.changeset/shy-hotels-tie.md @@ -0,0 +1,7 @@ +--- +"@quassel/frontend": minor +"@quassel/backend": minor +"@quassel/ui": minor +--- + +Allow clearing all entries from a questionnaire diff --git a/apps/backend/src/research/entries/entries.controller.ts b/apps/backend/src/research/entries/entries.controller.ts index 216396ff..9b6ca9b9 100644 --- a/apps/backend/src/research/entries/entries.controller.ts +++ b/apps/backend/src/research/entries/entries.controller.ts @@ -1,5 +1,5 @@ -import { Body, Controller, Delete, Get, Param, Patch, Post } from "@nestjs/common"; -import { ApiTags, ApiOperation, ApiUnprocessableEntityResponse } from "@nestjs/swagger"; +import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from "@nestjs/common"; +import { ApiTags, ApiOperation, ApiUnprocessableEntityResponse, ApiQuery } from "@nestjs/swagger"; import { ErrorResponseDto } from "../../common/dto/error.dto"; import { EntriesService } from "./entries.service"; import { EntryCreationDto, EntryResponseDto, EntryMutationDto } from "./entry.dto"; @@ -44,4 +44,11 @@ export class EntriesController { delete(@Param("id") id: string) { return this.entriesService.remove(+id); } + + @Delete() + @ApiQuery({ name: "questionnaireId", required: true, type: Number }) + @ApiOperation({ summary: "Delete all entries of a questionnaire" }) + deleteAll(@Query("questionnaireId") questionnaireId: number) { + return this.entriesService.removeAllFromQuestionnaire(questionnaireId); + } } diff --git a/apps/backend/src/research/entries/entries.service.ts b/apps/backend/src/research/entries/entries.service.ts index 38a2f7bf..e090f283 100644 --- a/apps/backend/src/research/entries/entries.service.ts +++ b/apps/backend/src/research/entries/entries.service.ts @@ -79,4 +79,8 @@ export class EntriesService { remove(id: number) { return this.em.remove(this.entryRepository.getReference(id)).flush(); } + + removeAllFromQuestionnaire(questionnaireId: number) { + return this.entryRepository.nativeDelete({ questionnaire: questionnaireId }); + } } diff --git a/apps/frontend/src/api.gen.ts b/apps/frontend/src/api.gen.ts index 4c07e192..996377dd 100644 --- a/apps/frontend/src/api.gen.ts +++ b/apps/frontend/src/api.gen.ts @@ -268,7 +268,8 @@ export interface paths { put?: never; /** Create a entry */ post: operations["EntriesController_create"]; - delete?: never; + /** Delete all entries of a questionnaire */ + delete: operations["EntriesController_deleteAll"]; options?: never; head?: never; patch?: never; @@ -1842,6 +1843,27 @@ export interface operations { }; }; }; + EntriesController_deleteAll: { + parameters: { + query: { + questionnaireId: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": number; + }; + }; + }; + }; EntriesController_get: { parameters: { query?: never; diff --git a/apps/frontend/src/routes/_auth/questionnaire/_questionnaire/$id/entries.tsx b/apps/frontend/src/routes/_auth/questionnaire/_questionnaire/$id/entries.tsx index 0d8931cd..ec64e8f2 100644 --- a/apps/frontend/src/routes/_auth/questionnaire/_questionnaire/$id/entries.tsx +++ b/apps/frontend/src/routes/_auth/questionnaire/_questionnaire/$id/entries.tsx @@ -1,4 +1,4 @@ -import { Button, Group, Modal, Stack, Title, useDisclosure, useForm } from "@quassel/ui"; +import { Button, Group, IconClearAll, modals, Stack, Title, useForm, Text } from "@quassel/ui"; import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"; import { i18n } from "../../../../../stores/i18n"; import { useStore } from "@nanostores/react"; @@ -14,9 +14,14 @@ const messages = i18n("questionnaireEntries", { addEntityLabel: "Add", notificationSuccessCreateLanguage: "Successfully add a new language.", notificationSuccessCreateCarer: "Successfully add a new carer.", - gapsDialogTitle: "Gaps detected in the calendar", + gapsDialogTitle: "Continue with gaps?", + gapsDialogDescription: "There were gaps detected in the calendar. Do you want to continue anyway or highlight the gaps?", gapsDialogContinueAnyway: "Continue anyway", gapsDialogHighlightGaps: "Highlight gaps", + confirmClearDialogTitle: "Clear all entries from this questionnaire?", + confirmClearDialogDescription: "When confirming, all entries from this questionnaires will be removed. This action can't be undone.", + confirmClearDialogCancel: "Cancel", + confirmClearDialogConfirm: "Clear all", }); export function Entries() { @@ -25,11 +30,12 @@ export function Entries() { const t = useStore(messages); - const { data: questionnaire } = $api.useSuspenseQuery("get", "/questionnaires/{id}", { params: { path: p } }); + const { data: questionnaire, refetch } = $api.useSuspenseQuery("get", "/questionnaires/{id}", { params: { path: p } }); + + const removeAllEntriesMutation = $api.useMutation("delete", "/entries", { onSuccess: () => refetch() }); const [gaps, setGaps] = useState(); const [highlightGaps, setHighlightGaps] = useState(false); - const [gapsDialogOpened, { open, close }] = useDisclosure(); const f = useForm<{ entries: components["schemas"]["EntryResponseDto"][] }>({ initialValues: { @@ -50,6 +56,27 @@ export function Entries() { n({ to: "/questionnaire/$id/remarks", params: p }); }; + const handleGapValidation = () => { + modals.openConfirmModal({ + title: t.gapsDialogTitle, + children: {t.gapsDialogDescription}, + labels: { cancel: t.gapsDialogHighlightGaps, confirm: t.gapsDialogContinueAnyway }, + confirmProps: { variant: "light" }, + cancelProps: { variant: "filled" }, + onConfirm: handleSubmit, + onCancel: () => setHighlightGaps(true), + }); + }; + + const handleClearEntries = () => { + modals.openConfirmModal({ + title: t.confirmClearDialogTitle, + children: {t.confirmClearDialogDescription}, + labels: { cancel: t.confirmClearDialogCancel, confirm: t.confirmClearDialogConfirm }, + onConfirm: () => removeAllEntriesMutation.mutate({ params: { query: { questionnaireId: questionnaire.id } } }), + }); + }; + useEffect(() => { f.setValues({ entries: questionnaire.entries }); @@ -57,36 +84,24 @@ export function Entries() { }, [questionnaire]); return ( - <> - - - - - - - - {questionnaire.title} - - - - - - - - - - - + + + + + + + + + + ); } diff --git a/libs/ui/package.json b/libs/ui/package.json index 8608537a..7bde7d31 100644 --- a/libs/ui/package.json +++ b/libs/ui/package.json @@ -33,6 +33,7 @@ "@mantine/dates": "7.17.2", "@mantine/form": "7.17.2", "@mantine/hooks": "7.17.2", + "@mantine/modals": "^7.17.2", "@mantine/notifications": "7.17.2", "@tabler/icons-react": "^3.31.0", "dayjs": "^1.11.13", diff --git a/libs/ui/src/index.ts b/libs/ui/src/index.ts index 8398d316..5a361356 100644 --- a/libs/ui/src/index.ts +++ b/libs/ui/src/index.ts @@ -99,6 +99,8 @@ export { useDisclosure, useFullscreen } from "@mantine/hooks"; export { notifications, type NotificationData } from "@mantine/notifications"; +export { modals } from "@mantine/modals"; + export { useForm, isInRange, isNotEmpty } from "@mantine/form"; export { @@ -123,6 +125,7 @@ export { IconReportAnalytics, IconX, IconInfoCircle, + IconClearAll, } from "@tabler/icons-react"; export { uzhColors } from "./theme/uzh"; diff --git a/libs/ui/src/theme/ThemeProvider.tsx b/libs/ui/src/theme/ThemeProvider.tsx index 3758bb3c..4c7569ad 100644 --- a/libs/ui/src/theme/ThemeProvider.tsx +++ b/libs/ui/src/theme/ThemeProvider.tsx @@ -6,6 +6,7 @@ import utc from "dayjs/plugin/utc"; import { DefaultMantineColor, MantineColorsTuple } from "@mantine/core"; import { Notifications } from "@mantine/notifications"; import { convertUZHColorsToMantine, UZHColor, uzhColors } from "./uzh"; +import { ModalsProvider } from "@mantine/modals"; dayjs.extend(utc); @@ -37,8 +38,10 @@ export function ThemeProvider({ children, ...args }: ThemeProviderProps) { return ( - - {children} + + + {children} + ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bdf0e37c..e9709afb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -296,6 +296,9 @@ importers: '@mantine/hooks': specifier: 7.17.2 version: 7.17.2(react@19.0.0) + '@mantine/modals': + specifier: ^7.17.2 + version: 7.17.2(@mantine/core@7.17.2(@mantine/hooks@7.17.2(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.2(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@mantine/notifications': specifier: 7.17.2 version: 7.17.2(@mantine/core@7.17.2(@mantine/hooks@7.17.2(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.2(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -1878,6 +1881,14 @@ packages: peerDependencies: react: ^18.x || ^19.x + '@mantine/modals@7.17.2': + resolution: {integrity: sha512-Ms8MYLJCZcxRnGfIQr4riGK2g5mpklxiEAU84vbptoAlQ2d5Iqu+CQ0XpDfamCQl/ltmPmYJYkrq52zhQWIS3w==} + peerDependencies: + '@mantine/core': 7.17.2 + '@mantine/hooks': 7.17.2 + react: ^18.x || ^19.x + react-dom: ^18.x || ^19.x + '@mantine/notifications@7.17.2': resolution: {integrity: sha512-vg0L8cmihz0ODg4WJ9MAyK06WPt/6g67ksIUFxd4F8RfdJbIMLTsNG9yWoSfuhtXenUg717KaA917IWLjDSaqw==} peerDependencies: @@ -8888,6 +8899,13 @@ snapshots: dependencies: react: 19.0.0 + '@mantine/modals@7.17.2(@mantine/core@7.17.2(@mantine/hooks@7.17.2(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.2(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@mantine/core': 7.17.2(@mantine/hooks@7.17.2(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@mantine/hooks': 7.17.2(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + '@mantine/notifications@7.17.2(@mantine/core@7.17.2(@mantine/hooks@7.17.2(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mantine/hooks@7.17.2(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@mantine/core': 7.17.2(@mantine/hooks@7.17.2(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)